Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(kumactl): allow apply of an entire directory #8647

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 130 additions & 48 deletions app/kumactl/cmd/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import (
"context"
"fmt"
"golang.org/x/exp/slices"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"

Expand All @@ -28,6 +30,8 @@
timeout = 10 * time.Second
)

var yamlExt = []string{".yaml", ".yml"}

type applyContext struct {
*kumactl_cmd.RootContext

Expand Down Expand Up @@ -63,66 +67,55 @@

var b []byte
var err error
var resources []model.Resource

if ctx.args.file == "-" {
b, err = io.ReadAll(cmd.InOrStdin())
if err != nil {
return err
}
} else {
if strings.HasPrefix(ctx.args.file, "http://") || strings.HasPrefix(ctx.args.file, "https://") {
client := &http.Client{
Timeout: timeout,
}
req, err := http.NewRequest("GET", ctx.args.file, nil)
if err != nil {
return errors.Wrap(err, "error creating new http request")
}
resp, err := client.Do(req)
if err != nil {
return errors.Wrap(err, "error with GET http request")
}
if resp.StatusCode != http.StatusOK {
return errors.Wrap(err, "error while retrieving URL")
}
defer resp.Body.Close()
b, err = io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "error while reading provided file")
}
} else {
b, err = os.ReadFile(ctx.args.file)
if err != nil {
return errors.Wrap(err, "error while reading provided file")
}
if len(b) == 0 {
return fmt.Errorf("no resource(s) passed to apply")
}
}
if len(b) == 0 {
return fmt.Errorf("no resource(s) passed to apply")
}
var resources []model.Resource
rawResources := yaml.SplitYAML(string(b))
for _, rawResource := range rawResources {
if len(rawResource) == 0 {
continue
r, err := bytesToResources(ctx, cmd, b)
if err != nil {
return errors.Wrap(err, "error parsing file to resources")
}
bytes := []byte(rawResource)
if len(ctx.args.vars) > 0 {
bytes = template.Render(rawResource, ctx.args.vars)
resources = append(resources, r...)
} else if strings.HasPrefix(ctx.args.file, "http://") || strings.HasPrefix(ctx.args.file, "https://") {
client := &http.Client{
Timeout: timeout,
}
res, err := rest_types.YAML.UnmarshalCore(bytes)
req, err := http.NewRequest("GET", ctx.args.file, nil)
if err != nil {
return errors.Wrap(err, "YAML contains invalid resource")
return errors.Wrap(err, "error creating new http request")
}
if err, msg := mesh.ValidateMetaBackwardsCompatible(res.GetMeta(), res.Descriptor().Scope); err.HasViolations() {
return err.OrNil()
} else if msg != "" {
if _, printErr := fmt.Fprintln(cmd.ErrOrStderr(), msg); printErr != nil {
return printErr
}
resp, err := client.Do(req)
if err != nil {
return errors.Wrap(err, "error with GET http request")
}
if resp.StatusCode != http.StatusOK {
return errors.Wrap(err, "error while retrieving URL")
}
defer resp.Body.Close()
b, err = io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "error while reading provided file")
}
r, err := bytesToResources(ctx, cmd, b)
if err != nil {
return errors.Wrap(err, "error parsing file to resources")
}
resources = append(resources, res)
resources = append(resources, r...)
} else {
// Process local yaml files
r, err := localFileToResources(ctx, cmd)
if err != nil {
return errors.Wrap(err, "error processing file")
}
resources = append(resources, r...)
}

var rs store.ResourceStore
if !ctx.args.dryRun {
rs, err = pctx.CurrentResourceStore()
Expand All @@ -137,7 +130,7 @@
return err
}
} else {
warnings, err := upsert(cmd.Context(), pctx.Runtime.Registry, rs, resource)

Check failure on line 133 in app/kumactl/cmd/apply/apply.go

View workflow job for this annotation

GitHub Actions / check

assignment mismatch: 2 variables but upsert returns 1 value

Check failure on line 133 in app/kumactl/cmd/apply/apply.go

View workflow job for this annotation

GitHub Actions / check

assignment mismatch: 2 variables but upsert returns 1 value
if err != nil {
return err
}
Expand All @@ -158,10 +151,99 @@
return cmd
}

func upsert(ctx context.Context, typeRegistry registry.TypeRegistry, rs store.ResourceStore, res model.Resource) ([]string, error) {
// localFileToResources reads and converts a local file into a list of model.Resource
// the local file could be a directory, in which case it processes all the yaml files in the directory
func localFileToResources(ctx *applyContext, cmd *cobra.Command) ([]model.Resource, error) {
var resources []model.Resource
file, err := os.Open(ctx.args.file)
if err != nil {
return nil, errors.Wrap(err, "error while opening provided file")
}
defer file.Close()
orgDir, _ := filepath.Split(ctx.args.file)

fileInfo, err := file.Stat()
if err != nil {
return nil, errors.Wrap(err, "error getting stats for the provided file")
}

var yamlFiles []string
if fileInfo.IsDir() {
for {
names, err := file.Readdirnames(10)
if err != nil {
if err == io.EOF {
break
} else {
return nil, errors.Wrap(err, "error reading file names in directory")
}
}
for _, n := range names {
if slices.Contains(yamlExt, filepath.Ext(n)) {
yamlFiles = append(yamlFiles, n)
}
}
}
} else {
if slices.Contains(yamlExt, filepath.Ext(fileInfo.Name())) {
yamlFiles = append(yamlFiles, fileInfo.Name())
}
// TODO should this check be added?
//else {
// return nil, fmt.Errorf("error the specified input file extension isn't yaml")
//}
}
var b []byte
for _, f := range yamlFiles {
joined := filepath.Join(orgDir, f)
b, err = os.ReadFile(joined)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("error while reading the provided file [%s]", f))
}
r, err := bytesToResources(ctx, cmd, b)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("error parsing file [%s] to resources", f))
}
resources = append(resources, r...)
}
if len(resources) == 0 {
return nil, fmt.Errorf("no resource(s) passed to apply")
}
return resources, nil
}

// bytesToResources converts a slice of bytes into a slice of model.Resource
func bytesToResources(ctx *applyContext, cmd *cobra.Command, fileBytes []byte) ([]model.Resource, error) {
var resources []model.Resource
rawResources := yaml.SplitYAML(string(fileBytes))
for _, rawResource := range rawResources {
if len(rawResource) == 0 {
continue
}
bytes := []byte(rawResource)
if len(ctx.args.vars) > 0 {
bytes = template.Render(rawResource, ctx.args.vars)
}
res, err := rest_types.YAML.UnmarshalCore(bytes)
if err != nil {
return nil, errors.Wrap(err, "YAML contains invalid resource")
}
if err, msg := mesh.ValidateMetaBackwardsCompatible(res.GetMeta(), res.Descriptor().Scope); err.HasViolations() {
return nil, err.OrNil()
} else if msg != "" {
if _, printErr := fmt.Fprintln(cmd.ErrOrStderr(), msg); printErr != nil {
return nil, printErr
}
}
resources = append(resources, res)
}
return resources, nil
}

func upsert(ctx context.Context, typeRegistry registry.TypeRegistry, rs store.ResourceStore, res model.Resource) error {
newRes, err := typeRegistry.NewObject(res.Descriptor().Name)
if err != nil {
return nil, err

Check failure on line 246 in app/kumactl/cmd/apply/apply.go

View workflow job for this annotation

GitHub Actions / check

too many return values

Check failure on line 246 in app/kumactl/cmd/apply/apply.go

View workflow job for this annotation

GitHub Actions / check

too many return values
}

var warnings []string
Expand All @@ -173,14 +255,14 @@
if err := rs.Get(ctx, newRes, store.GetByKey(meta.GetName(), meta.GetMesh())); err != nil {
if store.IsResourceNotFound(err) {
cerr := rs.Create(warnContext, res, store.CreateByKey(meta.GetName(), meta.GetMesh()), store.CreateWithLabels(meta.GetLabels()))
return warnings, cerr

Check failure on line 258 in app/kumactl/cmd/apply/apply.go

View workflow job for this annotation

GitHub Actions / check

too many return values

Check failure on line 258 in app/kumactl/cmd/apply/apply.go

View workflow job for this annotation

GitHub Actions / check

too many return values
} else {
return nil, err

Check failure on line 260 in app/kumactl/cmd/apply/apply.go

View workflow job for this annotation

GitHub Actions / check

too many return values
}
}
if err := newRes.SetSpec(res.GetSpec()); err != nil {
return nil, err

Check failure on line 264 in app/kumactl/cmd/apply/apply.go

View workflow job for this annotation

GitHub Actions / check

too many return values
}
uerr := rs.Update(warnContext, newRes, store.UpdateWithLabels(meta.GetLabels()))
return warnings, uerr

Check failure on line 267 in app/kumactl/cmd/apply/apply.go

View workflow job for this annotation

GitHub Actions / check

too many return values
}
Loading