diff --git a/app/kumactl/cmd/apply/apply.go b/app/kumactl/cmd/apply/apply.go index b2cd7c9e3ff0..473e3050d6fc 100644 --- a/app/kumactl/cmd/apply/apply.go +++ b/app/kumactl/cmd/apply/apply.go @@ -3,9 +3,11 @@ package apply import ( "context" "fmt" + "golang.org/x/exp/slices" "io" "net/http" "os" + "path/filepath" "strings" "time" @@ -28,6 +30,8 @@ const ( timeout = 10 * time.Second ) +var yamlExt = []string{".yaml", ".yml"} + type applyContext struct { *kumactl_cmd.RootContext @@ -63,66 +67,55 @@ $ kumactl apply -f https://example.com/resource.yaml 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() @@ -158,7 +151,96 @@ $ kumactl apply -f https://example.com/resource.yaml 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