// Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package commands import ( "encoding/json" "fmt" "io" "io/ioutil" "path/filepath" "strings" "github.com/go-openapi/spec" "github.com/spf13/cobra" "sigs.k8s.io/kustomize/cmd/config/ext" "sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/commands" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/setters" "sigs.k8s.io/kustomize/kyaml/setters2/settersutil" ) // NewCreateSetterRunner returns a command runner. func NewCreateSetterRunner(parent string) *CreateSetterRunner { r := &CreateSetterRunner{} set := &cobra.Command{ Use: "create-setter DIR NAME VALUE", Args: cobra.RangeArgs(2, 3), Short: commands.CreateSetterShort, Long: commands.CreateSetterLong, Example: commands.CreateSetterExamples, PreRunE: r.preRunE, RunE: r.runE, } set.Flags().StringVar(&r.Set.SetPartialField.Setter.Value, "value", "", "optional flag, alternative to specifying the value as an argument. e.g. used to specify values that start with '-'") set.Flags().StringVar(&r.Set.SetPartialField.SetBy, "set-by", "", "record who the field was default by.") set.Flags().StringVar(&r.Set.SetPartialField.Description, "description", "", "record a description for the current setter value.") set.Flags().StringVar(&r.Set.SetPartialField.Field, "field", "", "name of the field to set, a suffix of the path to the field, or the full"+ " path to the field. Default is to match all fields.") set.Flags().StringVar(&r.Set.ResourceMeta.Name, "name", "", "name of the Resource on which to create the setter.") set.Flags().MarkHidden("name") set.Flags().StringVar(&r.Set.ResourceMeta.Kind, "kind", "", "kind of the Resource on which to create the setter.") set.Flags().MarkHidden("kind") set.Flags().StringVar(&r.Set.SetPartialField.Type, "type", "", "OpenAPI field type for the setter -- e.g. integer,boolean,string.") set.Flags().BoolVar(&r.Set.SetPartialField.Partial, "partial", false, "create a partial setter for only part of the field value.") set.Flags().MarkHidden("partial") set.Flags().StringVar(&setterVersion, "version", "", "use this version of the setter format") set.Flags().BoolVar(&r.CreateSetter.Required, "required", false, "indicates that this setter must be set by package consumer before live apply/preview") set.Flags().StringVar(&r.SchemaPath, "schema-path", "", `openAPI schema file path for setter constraints -- file content `+ `e.g. {"type": "string", "maxLength": 15, "enum": ["allowedValue1", "allowedValue2"]}`) set.Flags().BoolVarP(&r.CreateSetter.RecurseSubPackages, "recurse-subpackages", "R", false, "creates setter recursively in all the nested subpackages") set.Flags().MarkHidden("version") fixDocs(parent, set) r.Command = set return r } func CreateSetterCommand(parent string) *cobra.Command { return NewCreateSetterRunner(parent).Command } type CreateSetterRunner struct { Command *cobra.Command Set setters.CreateSetter CreateSetter settersutil.SetterCreator OpenAPIFile string SchemaPath string } func (r *CreateSetterRunner) runE(c *cobra.Command, args []string) error { return handleError(c, r.createSetter(c, args)) } func (r *CreateSetterRunner) preRunE(c *cobra.Command, args []string) error { valueSetFromFlag := c.Flag("value").Changed var err error r.Set.SetPartialField.Setter.Name = args[1] r.CreateSetter.Name = args[1] if valueSetFromFlag { r.CreateSetter.FieldValue = r.Set.SetPartialField.Setter.Value } else if len(args) > 2 { r.Set.SetPartialField.Setter.Value = args[2] r.CreateSetter.FieldValue = args[2] } r.CreateSetter.FieldName, err = c.Flags().GetString("field") if err != nil { return err } if setterVersion == "" { if len(args) == 2 && r.Set.SetPartialField.Type == "array" && c.Flag("field").Changed { setterVersion = "v2" } else if err := initSetterVersion(c, args); err != nil { return err } } if r.Set.SetPartialField.Type != "array" && !c.Flag("value").Changed && len(args) < 3 { return errors.Errorf("setter name and value must be provided, " + "value can either be an argument or can be passed as a flag --value") } if setterVersion == "v2" { r.CreateSetter.Description = r.Set.SetPartialField.Description r.CreateSetter.SetBy = r.Set.SetPartialField.SetBy r.CreateSetter.Type = r.Set.SetPartialField.Type err = r.processSchema() if err != nil { return err } if r.CreateSetter.Type == "array" { if !c.Flag("field").Changed { return errors.Errorf("field flag must be set for array type setters") } } } return nil } func (r *CreateSetterRunner) processSchema() error { sc, err := schemaFromFile(r.SchemaPath) if err != nil { return err } flagType := r.CreateSetter.Type var schemaType string switch { // json schema allows more than one type to be specified, but openapi // only allows one. So we follow the openapi convention. case len(sc.Type) > 1: return errors.Errorf("only one type is supported: %s", strings.Join(sc.Type, ", ")) case len(sc.Type) == 1: schemaType = sc.Type[0] } // Since type can be set both through the schema file and through the // --type flag, we make sure the same value is set in both places. If they // are both set with different values, we return an error. switch { case flagType == "" && schemaType != "": r.CreateSetter.Type = schemaType case flagType != "" && schemaType == "": sc.Type = []string{flagType} case flagType != "" && schemaType != "": if flagType != schemaType { return errors.Errorf("type provided in type flag (%s) and in schema (%s) doesn't match", r.CreateSetter.Type, sc.Type[0]) } } // Only marshal the properties in SchemaProps. This means any fields in // the schema file that isn't recognized will be dropped. // TODO: Consider if we should return an error here instead of just dropping // the unknown fields. b, err := json.Marshal(sc.SchemaProps) if err != nil { return errors.Errorf("error marshalling schema: %v", err) } r.CreateSetter.Schema = string(b) return nil } func (r *CreateSetterRunner) createSetter(c *cobra.Command, args []string) error { if setterVersion == "v2" { e := executeCmdOnPkgs{ needOpenAPI: true, writer: c.OutOrStdout(), rootPkgPath: args[0], recurseSubPackages: r.CreateSetter.RecurseSubPackages, cmdRunner: r, } err := e.execute() if err != nil { return handleError(c, err) } return nil } rw := &kio.LocalPackageReadWriter{PackagePath: args[0]} err := kio.Pipeline{ Inputs: []kio.Reader{rw}, Filters: []kio.Filter{&r.Set}, Outputs: []kio.Writer{rw}}.Execute() if err != nil { return err } return nil } func (r *CreateSetterRunner) executeCmd(w io.Writer, pkgPath string) error { openAPIFileName, err := ext.OpenAPIFileName() if err != nil { return err } r.CreateSetter = settersutil.SetterCreator{ Name: r.CreateSetter.Name, SetBy: r.CreateSetter.SetBy, Description: r.CreateSetter.Description, Type: r.CreateSetter.Type, Schema: r.CreateSetter.Schema, FieldName: r.CreateSetter.FieldName, FieldValue: r.CreateSetter.FieldValue, Required: r.CreateSetter.Required, RecurseSubPackages: r.CreateSetter.RecurseSubPackages, OpenAPIFileName: openAPIFileName, OpenAPIPath: filepath.Join(pkgPath, openAPIFileName), ResourcesPath: pkgPath, } err = r.CreateSetter.Create() if err != nil { // return err if RecurseSubPackages is false if !r.CreateSetter.RecurseSubPackages { return err } else { // print error message and continue if RecurseSubPackages is true fmt.Fprintf(w, "%s\n", err.Error()) } } else { fmt.Fprintf(w, "created setter %q\n", r.CreateSetter.Name) } return nil } // schemaFromFile reads the contents from schemaPath and returns schema func schemaFromFile(schemaPath string) (*spec.Schema, error) { sc := &spec.Schema{} if schemaPath == "" { return sc, nil } sch, err := ioutil.ReadFile(schemaPath) if err != nil { return sc, err } if len(sch) == 0 { return sc, nil } err = sc.UnmarshalJSON(sch) if err != nil { return sc, errors.Errorf("unable to parse schema: %v", err) } return sc, nil }