diff --git a/cmd/config/configcobra/cmds.go b/cmd/config/configcobra/cmds.go index f6c85122f..0fd9c86bd 100644 --- a/cmd/config/configcobra/cmds.go +++ b/cmd/config/configcobra/cmds.go @@ -76,6 +76,8 @@ func NewConfigCommand(name string) *cobra.Command { root.AddCommand(commands.MergeCommand(name)) root.AddCommand(commands.CountCommand(name)) root.AddCommand(commands.RunFnCommand(name)) + root.AddCommand(commands.SubCommand(name)) + root.AddCommand(commands.SubSetCommand(name)) root.AddCommand(&cobra.Command{ Use: "docs-merge", diff --git a/cmd/config/docs/commands/sub.md b/cmd/config/docs/commands/sub.md new file mode 100644 index 000000000..38540fe9f --- /dev/null +++ b/cmd/config/docs/commands/sub.md @@ -0,0 +1,98 @@ +## set + +[Alpha] Set values on Resources fields by substituting values. + +### Synopsis + +Set values on Resources fields by substituting predefined markers for new values. + +`set` looks for markers specified on Resource fields and substitute a new user defined +value for the existing value. + +`set` maybe be used to: + +- edit configuration programmatically from the cli or scripts +- create reusable bundles of configuration + + DIR + + A directory containing Resource configuration. + + NAME + + Optional. The name of the substitution to perform or display. + + VALUE + + Optional. The new value to substitute into the field. + + +To print the possible substitutions for the Resources in a directory, run `set` on +a directory -- e.g. `kustomize config set DIR/`. + +#### Tips + +- A description of the value may be specified with `--description`. +- An owner for the field's value may be defined with `--owned-by`. +- Prevent overriding previous substitutions with `--override=false`. +- Revert previous substitutions with `--revert`. +- Create substitutions on Kustomization.yaml's, patches, etc + +When overriding or reverting previous substitutions, the description and owner are left +unmodified unless specified with flags. + +To create a substitution for a field see: `kustomize help config set create` + +### Examples + + Resource YAML: Name substitution + + # dir/resources.yaml + ... + metadata: + name: PREFIX-app1 # {"substitutions":[{"name":"prefix","marker":"PREFIX-"}]} + ... + --- + ... + metadata: + name: PREFIX-app2 # {"substitutions":[{"name":"prefix","marker":"PREFIX-"}]} + ... + + Show substitutions: Show the possible substitutions + + $ config set dir + NAME DESCRIPTION VALUE TYPE COUNT SUBSTITUTED OWNER + prefix '' PREFIX- string 2 false + + Perform substitution: set a new value, owner and description + + $ config set dir prefix "test-" --description "test environment" --owned-by "dev" + performed 2 substitutions + + Show substitutions: Show the new values + + $ config set dir + NAME DESCRIPTION VALUE TYPE COUNT SUBSTITUTED OWNER + prefix 'test environment' test- string 2 true dev + + New Resource YAML: + + # dir/resources.yaml + ... + metadata: + name: test-app1 # {"substitutions":[{"name":"prefix","marker":"PREFIX-","value":"test-"}],"ownedBy":"dev","description":"test environment"} + ... + --- + ... + metadata: + name: test-app2 # {"substitutions":[{"name":"prefix","marker":"PREFIX-","value":"test-"}],"ownedBy":"dev","description":"test environment"} + ... + + Revert substitution: + + config set dir prefix --revert + performed 2 substitutions + + config set dir + NAME DESCRIPTION VALUE TYPE COUNT SUBSTITUTED OWNER + prefix 'test environment' PREFIX- string 2 false dev diff --git a/cmd/config/docs/commands/subset.md b/cmd/config/docs/commands/subset.md new file mode 100644 index 000000000..1a3f5ca00 --- /dev/null +++ b/cmd/config/docs/commands/subset.md @@ -0,0 +1,167 @@ +## sub-set-marker + +[Alpha] Create a new substitution for a Resource field + +### Synopsis + +Create a new substitution for a Resource field -- recognized by `kustomize config set`. + + DIR + + A directory containing Resource configuration. + + NAME + + The name of the substitution to create. + + VALUE + + The current value of the field, or a substring of the field. + +#### Tips: Picking Good Marker + +Substitutions may be defined by directly editing yaml **or** by running `kustomize config set create` +to create a new substitution. + +Given the YAML: + + # resource.yaml + apiVersion: v1 + kind: Service + metadata: + ... + spec: + ... + ports: + ... + - name: http + port: 8080 + ... + +Create a new set marker: + + # create a substitution for ports + $ kustomize config set create dir/ http-port 8080 --type "int" --field "port" + +Modified YAML: + + # resource.yaml + apiVersion: v1 + kind: Service + metadata: + ... + spec: + ... + ports: + ... + - name: http + port: 8080 # {"substitutions":[{"name":"port","marker":"[MARKER]"}],"type":"int"} + ... + +Change the value using the `set` command: + + # change the http-port value to 8081 + $ kustomize config set dir/ http-port 8081 + +Resources fields with a field name matching `--field` and field value matching `VALUE` will +have a line comment added marking this field as settable. + +Substitution markers may be: + +- valid field values (e.g. `8080` for a port) + - Note: `008080` would be preferred because it is more recognizable as a marker +- invalid values that adhere to the schema (e.g. `0000` for a port) +- values that do not adhere to the schema (e.g. `[PORT]` for port) + +Markers **SHOULD be clearly identifiable as a marker and either**: + +- **adhere to the field schema** -- e.g. use a valid value + + + port: 008080 # {"substitutions":[{"name":"port","marker":"008080"}],"type":"int"} + +- **be pre-filled in with a value** -- e.g. set the value when setting the marker + + + port: 8080 # {"substitutions":[{"name":"port","marker":"[MARKER]","value":"8080""}],"type":"int"} + +**Note:** The important thing is that in both cases the Resource configuration may be directly +applied to a cluster and validated by tools without the tool knowing about the substitution +marker. + +The difference between the preceding examples is that: + +- the former will be shown as `SUBSTITUTED=false` (`config sub dir/` exits non-0) +- the latter with show up as `SUBSTITUTED=true` (`config sub dir/` exits 0) + +When choosing the which to use, consider that checks for unsubstituted values MAY be +configured as pre-commit checks -- if you want to these checks to fail if the value +hasn't been substituted, then don't specify a `value`. + +Markers which are invalid field values MAY be chosen in cases where it is preferred to have +the create or update request fail rather than succeed if the substitution has not yet been +performed. + +A substitution may be a substring of the full field: + + $ kustomize config set create dir/ app-image-tag v1.0.01 --type "string" --field "image" + + image: gcr.io/example/app:v1.0.1 # {"substitutions":[{"name":"app-image-tag","marker":"[MARKER]","value":"v1.0.1"}]} + + +A single field value may have multiple substitutions applied to it: + + name: PREFIX-app-SUFFIX # {"substitutions":[{"name":"prefix","marker":"PREFIX-"},{"name":"suffix","marker":"-SUFFIX"}]} + +#### Substitution Format + +Substitutions are defined as json encoded FieldMeta comments on fields. + +FieldMeta Schema read by `sub`: + + { + "title": "FieldMeta", + "type": "object", + "properties": { + "substitutions": { + "type": "array", + "description": "Possible substitutions that may be performed against this field.", + "items": { + "type": "object", + "properties": { + "name": "Name of the substitution.", + "marker": "Marker for the value to be substituted.", + "value": "Current substituted value" + } + } + }, + "type": { + "type": "string", + "description": "The value type. Defaults to string." + "enum": ["string", "int", "float", "bool"] + }, + "description": { + "type": "string", + "description": "A description of the field's current value. Optional." + }, + "ownedBy": { + "type": "string", + "description": "The current owner of the field. Optional." + }, + } + } + +### Examples + + # set a substitution for port fields matching "8080" + kustomize config sub create dir/ port 8080 --type "int" --field port \ + --description "default port used by the app" + + # set a substitution for port fields matching "8080", using "0000" as a marker. + kustomize config sub dir/ port 8080 --marker "0000" --type "int" \ + --field port --description "default port used by the app" + + # substitute a substring of a field rather than the full field -- e.g. only the + # image tag, not the full image + kustomize config sub dir/ app-image-tag v1.0.1 --type "string" --substring \ + --field port --description "current stable release" \ No newline at end of file diff --git a/cmd/config/go.mod b/cmd/config/go.mod index 1378e6029..487ad6c86 100644 --- a/cmd/config/go.mod +++ b/cmd/config/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/go-errors/errors v1.0.1 + github.com/olekukonko/tablewriter v0.0.4 github.com/posener/complete/v2 v2.0.1-alpha.12 github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.5 diff --git a/cmd/config/go.sum b/cmd/config/go.sum index a18430907..6a2ddd573 100644 --- a/cmd/config/go.sum +++ b/cmd/config/go.sum @@ -61,6 +61,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= @@ -72,6 +74,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/cmd/config/internal/commands/cmdsub.go b/cmd/config/internal/commands/cmdsub.go new file mode 100644 index 000000000..ebb8e6f19 --- /dev/null +++ b/cmd/config/internal/commands/cmdsub.go @@ -0,0 +1,162 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "fmt" + "os" + + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" + "sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/commands" + "sigs.k8s.io/kustomize/cmd/config/internal/sub" + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/kio" +) + +// NewSubRunner returns a command runner. +func NewSubRunner(parent string) *SubRunner { + r := &SubRunner{} + c := &cobra.Command{ + Use: "set DIR [NAME] [VALUE]", + Args: cobra.RangeArgs(1, 3), + Short: commands.SubShort, + Long: commands.SubLong, + Example: commands.SubExamples, + Aliases: []string{"sub"}, + PreRunE: r.preRunE, + RunE: r.runE, + } + c.Flags().BoolVar(&r.Perform.Override, "override", true, + "override previously substituted values.") + c.Flags().BoolVar(&r.Perform.Revert, "revert", false, + "override previously substituted values.") + fixDocs(parent, c) + r.Command = c + c.AddCommand(SubSetCommand(parent)) + return r +} + +func SubCommand(parent string) *cobra.Command { + return NewSubRunner(parent).Command +} + +type SubRunner struct { + Command *cobra.Command + Lookup sub.LookupSubstitutions + Perform sub.PerformSubstitutions +} + +func (r *SubRunner) preRunE(c *cobra.Command, args []string) error { + if len(args) > 1 { + r.Perform.Name = args[1] + r.Lookup.Name = args[1] + } + if len(args) > 2 { + r.Perform.NewValue = args[2] + } + if len(args) < 2 && r.Perform.Revert { + return errors.Errorf("must specify NAME with --revert") + } + + var mutex int + if r.Perform.Revert { + mutex++ + } + if r.Perform.Override { + mutex++ + } + if mutex > 1 { + return errors.Errorf("--revert, --override are mutually exclusive") + } + + return nil +} + +func (r *SubRunner) runE(c *cobra.Command, args []string) error { + + if len(args) == 3 { + return handleError(c, r.perform(c, args)) + } + if len(args) == 2 && r.Perform.Revert { + return handleError(c, r.perform(c, args)) + } + + return handleError(c, r.lookup(c, args)) +} + +func (r *SubRunner) lookup(c *cobra.Command, args []string) error { + // lookup the substitutions + err := kio.Pipeline{ + Inputs: []kio.Reader{&kio.LocalPackageReader{PackagePath: args[0]}}, + Filters: []kio.Filter{&r.Lookup}, + }.Execute() + if err != nil { + return err + } + + remaining := false + table := tablewriter.NewWriter(c.OutOrStdout()) + table.SetRowLine(false) + table.SetBorder(false) + table.SetHeaderLine(false) + table.SetColumnSeparator(" ") + table.SetCenterSeparator(" ") + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetHeader([]string{ + "NAME", "DESCRIPTION", "VALUE", "TYPE", "COUNT", "SUBSTITUTED", "OWNER", + }) + for i := range r.Lookup.SubstitutionCounts { + s := r.Lookup.SubstitutionCounts[i] + remaining = remaining || s.Count > s.CountComplete + v := s.CurrentValue + if s.CurrentValue == "" { + v = s.Marker + } + table.Append([]string{ + s.Name, + "'" + s.Description + "'", + v, + fmt.Sprintf("%v", s.Type), + fmt.Sprintf("%d", s.Count), + fmt.Sprintf("%v", s.Count == s.CountComplete), + s.OwnedBy, + }) + } + table.Render() + + if remaining { + os.Exit(1) + } + return nil +} + +// perform the substitutions +func (r *SubRunner) perform(c *cobra.Command, args []string) error { + rw := &kio.LocalPackageReadWriter{ + PackagePath: args[0], + } + // perform the substitutions in the package + err := kio.Pipeline{ + Inputs: []kio.Reader{rw}, + Filters: []kio.Filter{&r.Perform}, + Outputs: []kio.Writer{rw}, + }.Execute() + if err != nil { + return err + } + fmt.Fprintf(c.OutOrStdout(), "performed %d substitutions\n", r.Perform.Count) + return nil +} diff --git a/cmd/config/internal/commands/cmdsubcreate.go b/cmd/config/internal/commands/cmdsubcreate.go new file mode 100644 index 000000000..ee9378b64 --- /dev/null +++ b/cmd/config/internal/commands/cmdsubcreate.go @@ -0,0 +1,93 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "github.com/spf13/cobra" + "sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/commands" + "sigs.k8s.io/kustomize/cmd/config/internal/sub" + "sigs.k8s.io/kustomize/kyaml/kio" +) + +// NewSubSetRunner returns a command runner. +func NewSubSetRunner(parent string) *SubSetRunner { + r := &SubSetRunner{} + set := &cobra.Command{ + Use: "create PKG_DIR NAME [VALUE]", + Args: cobra.ExactArgs(3), + Short: commands.SubsetShort, + Long: commands.SubsetLong, + Example: commands.SubsetExamples, + PreRunE: r.preRunE, + RunE: r.runE, + } + set.Flags().StringVar(&r.Set.Marker.OwnedBy, "owned-by", "", + "set this owner on for the current value.") + set.Flags().StringVar(&r.Set.Marker.Description, "description", "", + "set this description for the current value description.") + set.Flags().StringVar(&r.Set.Marker.Substitution.Marker, "marker", "[MARKER]", + "use this marker.") + set.Flags().StringVar(&r.Set.Marker.Field, "field", "", + "name of the field to set -- e.g. --field port") + set.Flags().StringVar(&r.Set.ResourceMeta.Name, "name", "", + "name of the Resource on which to set the substitution.") + set.Flags().StringVar(&r.Set.ResourceMeta.Kind, "kind", "", + "kind of the Resource on which to set substitution.") + set.Flags().StringVar(&r.Set.Marker.Type, "type", "", + "field type -- e.g. int,float,bool,string.") + set.Flags().BoolVar(&r.Set.Marker.PartialMatch, "substring", false, + "if true, the value may be a substring of the current value.") + fixDocs(parent, set) + set.MarkFlagRequired("type") + set.MarkFlagRequired("field") + r.Command = set + return r +} + +func SubSetCommand(parent string) *cobra.Command { + return NewSubSetRunner(parent).Command +} + +type SubSetRunner struct { + Command *cobra.Command + Set sub.SetSubstitutionMarker +} + +func (r *SubSetRunner) runE(c *cobra.Command, args []string) error { + return handleError(c, r.set(c, args)) +} + +func (r *SubSetRunner) preRunE(c *cobra.Command, args []string) error { + r.Set.Marker.Substitution.Name = args[1] + r.Set.Marker.Substitution.Value = args[2] + return nil +} + +// perform the substitutions +func (r *SubSetRunner) set(c *cobra.Command, args []string) error { + rw := &kio.LocalPackageReadWriter{ + PackagePath: args[0], + } + // add the substitution marker to the Resource + err := kio.Pipeline{ + Inputs: []kio.Reader{rw}, + Filters: []kio.Filter{&r.Set}, + Outputs: []kio.Writer{rw}, + }.Execute() + if err != nil { + return err + } + return nil +} diff --git a/cmd/config/internal/generateddocs/commands/docs.go b/cmd/config/internal/generateddocs/commands/docs.go index fef935bfc..598a8c676 100644 --- a/cmd/config/internal/generateddocs/commands/docs.go +++ b/cmd/config/internal/generateddocs/commands/docs.go @@ -185,6 +185,263 @@ order they appear in the file). var RunFnsExamples = ` kustomize config run example/` +var SubShort = `[Alpha] Set values on Resources fields by substituting values.` +var SubLong = ` +Set values on Resources fields by substituting predefined markers for new values. + +` + "`" + `set` + "`" + ` looks for markers specified on Resource fields and substitute a new user defined +value for the existing value. + +` + "`" + `set` + "`" + ` maybe be used to: + +- edit configuration programmatically from the cli or scripts +- create reusable bundles of configuration + + DIR + + A directory containing Resource configuration. + + NAME + + Optional. The name of the substitution to perform or display. + + VALUE + + Optional. The new value to substitute into the field. + + +To print the possible substitutions for the Resources in a directory, run ` + "`" + `set` + "`" + ` on +a directory -- e.g. ` + "`" + `kustomize config set DIR/` + "`" + `. + +#### Tips + +- A description of the value may be specified with ` + "`" + `--description` + "`" + `. +- An owner for the field's value may be defined with ` + "`" + `--owned-by` + "`" + `. +- Prevent overriding previous substitutions with ` + "`" + `--override=false` + "`" + `. +- Revert previous substitutions with ` + "`" + `--revert` + "`" + `. +- Create substitutions on Kustomization.yaml's, patches, etc + +When overriding or reverting previous substitutions, the description and owner are left +unmodified unless specified with flags. + +To create a substitution for a field see: ` + "`" + `kustomize help config set create` + "`" + ` +` +var SubExamples = ` + Resource YAML: Name substitution + + # dir/resources.yaml + ... + metadata: + name: PREFIX-app1 # {"substitutions":[{"name":"prefix","marker":"PREFIX-"}]} + ... + --- + ... + metadata: + name: PREFIX-app2 # {"substitutions":[{"name":"prefix","marker":"PREFIX-"}]} + ... + + Show substitutions: Show the possible substitutions + + $ config set dir + NAME DESCRIPTION VALUE TYPE COUNT SUBSTITUTED OWNER + prefix '' PREFIX- string 2 false + + Perform substitution: set a new value, owner and description + + $ config set dir prefix "test-" --description "test environment" --owned-by "dev" + performed 2 substitutions + + Show substitutions: Show the new values + + $ config set dir + NAME DESCRIPTION VALUE TYPE COUNT SUBSTITUTED OWNER + prefix 'test environment' test- string 2 true dev + + New Resource YAML: + + # dir/resources.yaml + ... + metadata: + name: test-app1 # {"substitutions":[{"name":"prefix","marker":"PREFIX-","value":"test-"}],"ownedBy":"dev","description":"test environment"} + ... + --- + ... + metadata: + name: test-app2 # {"substitutions":[{"name":"prefix","marker":"PREFIX-","value":"test-"}],"ownedBy":"dev","description":"test environment"} + ... + + Revert substitution: + + config set dir prefix --revert + performed 2 substitutions + + config set dir + NAME DESCRIPTION VALUE TYPE COUNT SUBSTITUTED OWNER + prefix 'test environment' PREFIX- string 2 false dev ` + +var SubsetShort = `[Alpha] Create a new substitution for a Resource field` +var SubsetLong = ` +Create a new substitution for a Resource field -- recognized by ` + "`" + `kustomize config set` + "`" + `. + + DIR + + A directory containing Resource configuration. + + NAME + + The name of the substitution to create. + + VALUE + + The current value of the field, or a substring of the field. + +#### Tips: Picking Good Marker + +Substitutions may be defined by directly editing yaml **or** by running ` + "`" + `kustomize config set create` + "`" + ` +to create a new substitution. + +Given the YAML: + + # resource.yaml + apiVersion: v1 + kind: Service + metadata: + ... + spec: + ... + ports: + ... + - name: http + port: 8080 + ... + +Create a new set marker: + + # create a substitution for ports + $ kustomize config set create dir/ http-port 8080 --type "int" --field "port" + +Modified YAML: + + # resource.yaml + apiVersion: v1 + kind: Service + metadata: + ... + spec: + ... + ports: + ... + - name: http + port: 8080 # {"substitutions":[{"name":"port","marker":"[MARKER]"}],"type":"int"} + ... + +Change the value using the ` + "`" + `set` + "`" + ` command: + + # change the http-port value to 8081 + $ kustomize config set dir/ http-port 8081 + +Resources fields with a field name matching ` + "`" + `--field` + "`" + ` and field value matching ` + "`" + `VALUE` + "`" + ` will +have a line comment added marking this field as settable. + +Substitution markers may be: + +- valid field values (e.g. ` + "`" + `8080` + "`" + ` for a port) + - Note: ` + "`" + `008080` + "`" + ` would be preferred because it is more recognizable as a marker +- invalid values that adhere to the schema (e.g. ` + "`" + `0000` + "`" + ` for a port) +- values that do not adhere to the schema (e.g. ` + "`" + `[PORT]` + "`" + ` for port) + +Markers **SHOULD be clearly identifiable as a marker and either**: + +- **adhere to the field schema** -- e.g. use a valid value + + + port: 008080 # {"substitutions":[{"name":"port","marker":"008080"}],"type":"int"} + +- **be pre-filled in with a value** -- e.g. set the value when setting the marker + + + port: 8080 # {"substitutions":[{"name":"port","marker":"[MARKER]","value":"8080""}],"type":"int"} + +**Note:** The important thing is that in both cases the Resource configuration may be directly +applied to a cluster and validated by tools without the tool knowing about the substitution +marker. + +The difference between the preceding examples is that: + +- the former will be shown as ` + "`" + `SUBSTITUTED=false` + "`" + ` (` + "`" + `config sub dir/` + "`" + ` exits non-0) +- the latter with show up as ` + "`" + `SUBSTITUTED=true` + "`" + ` (` + "`" + `config sub dir/` + "`" + ` exits 0) + +When choosing the which to use, consider that checks for unsubstituted values MAY be +configured as pre-commit checks -- if you want to these checks to fail if the value +hasn't been substituted, then don't specify a ` + "`" + `value` + "`" + `. + +Markers which are invalid field values MAY be chosen in cases where it is preferred to have +the create or update request fail rather than succeed if the substitution has not yet been +performed. + +A substitution may be a substring of the full field: + + $ kustomize config set create dir/ app-image-tag v1.0.01 --type "string" --field "image" + + image: gcr.io/example/app:v1.0.1 # {"substitutions":[{"name":"app-image-tag","marker":"[MARKER]","value":"v1.0.1"}]} + + +A single field value may have multiple substitutions applied to it: + + name: PREFIX-app-SUFFIX # {"substitutions":[{"name":"prefix","marker":"PREFIX-"},{"name":"suffix","marker":"-SUFFIX"}]} + +#### Substitution Format + +Substitutions are defined as json encoded FieldMeta comments on fields. + +FieldMeta Schema read by ` + "`" + `sub` + "`" + `: + + { + "title": "FieldMeta", + "type": "object", + "properties": { + "substitutions": { + "type": "array", + "description": "Possible substitutions that may be performed against this field.", + "items": { + "type": "object", + "properties": { + "name": "Name of the substitution.", + "marker": "Marker for the value to be substituted.", + "value": "Current substituted value" + } + } + }, + "type": { + "type": "string", + "description": "The value type. Defaults to string." + "enum": ["string", "int", "float", "bool"] + }, + "description": { + "type": "string", + "description": "A description of the field's current value. Optional." + }, + "ownedBy": { + "type": "string", + "description": "The current owner of the field. Optional." + }, + } + } +` +var SubsetExamples = ` + # set a substitution for port fields matching "8080" + kustomize config sub create dir/ port 8080 --type "int" --field port \ + --description "default port used by the app" + + # set a substitution for port fields matching "8080", using "0000" as a marker. + kustomize config sub dir/ port 8080 --marker "0000" --type "int" \ + --field port --description "default port used by the app" + + # substitute a substring of a field rather than the full field -- e.g. only the + # image tag, not the full image + kustomize config sub dir/ app-image-tag v1.0.1 --type "string" --substring \ + --field port --description "current stable release"` + var TreeShort = `[Alpha] Display Resource structure from a directory or stdin.` var TreeLong = ` [Alpha] Display Resource structure from a directory or stdin. diff --git a/cmd/config/internal/sub/addkio.go b/cmd/config/internal/sub/addkio.go new file mode 100644 index 000000000..dff71f983 --- /dev/null +++ b/cmd/config/internal/sub/addkio.go @@ -0,0 +1,39 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package sub + +import ( + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +var _ kio.Filter = &SetSubstitutionMarker{} + +// Sub performs substitutions +type SetSubstitutionMarker struct { + // Marker is the marker to set + Marker Marker + + // ResourceMeta defines the Resource to set the marker on + ResourceMeta yaml.ResourceMeta +} + +func (s *SetSubstitutionMarker) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) { + for i := range input { + m, err := input[i].GetMeta() + if err != nil { + return nil, err + } + if s.ResourceMeta.Name != "" && m.Name != s.ResourceMeta.Name { + continue + } + if s.ResourceMeta.Kind != "" && m.Kind != s.ResourceMeta.Kind { + continue + } + if err := input[i].PipeE(&s.Marker); err != nil { + return nil, err + } + } + return input, nil +} diff --git a/cmd/config/internal/sub/addyaml.go b/cmd/config/internal/sub/addyaml.go new file mode 100644 index 000000000..118d054da --- /dev/null +++ b/cmd/config/internal/sub/addyaml.go @@ -0,0 +1,105 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package sub + +import ( + "strings" + + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/fieldmeta" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +var _ yaml.Filter = &Marker{} + +// substituteResource substitutes a Marker value on a field +type Marker struct { + // Path is the path of the field to add the substitution for + Field string + + // Substitution is the substitution to add + Substitution fieldmeta.Substitution + + // PartialMatch if true will match if the Substitution value is a substring of the current + // value. + PartialMatch bool + + Description string + OwnedBy string + Type string + + // currentFieldName is the name of the current field being processed + currentFieldName string +} + +// Filter performs the substitutions for a single object +func (m *Marker) Filter(object *yaml.RNode) (*yaml.RNode, error) { + switch object.YNode().Kind { + case yaml.DocumentNode: + return m.Filter(yaml.NewRNode(object.YNode().Content[0])) + case yaml.MappingNode: + return object, object.VisitFields(func(node *yaml.MapNode) error { + // set the current field name + n := m.currentFieldName + defer func() { m.currentFieldName = n }() + m.currentFieldName = node.Key.YNode().Value + _, err := m.Filter(node.Value) + return err + }) + case yaml.SequenceNode: + return object, object.VisitElements(func(node *yaml.RNode) error { + _, err := m.Filter(node) + return err + }) + case yaml.ScalarNode: + if m.currentFieldName != m.Field { + return object, nil + } + if err := m.createSub(object); err != nil { + return nil, err + } + return object, nil + default: + return object, nil + } +} + +func (as *Marker) createSub(field *yaml.RNode) error { + // doesn't match the supplied value + if field.YNode().Value != as.Substitution.Value { + if !as.PartialMatch || !strings.Contains(field.YNode().Value, as.Substitution.Value) { + return nil + } + } + + fm := fieldmeta.FieldMeta{} + if err := fm.Read(field); err != nil { + return errors.Wrap(err) + } + fm.OwnedBy = as.OwnedBy + fm.Description = as.Description + fm.Type = fieldmeta.FieldValueType(as.Type) + if as.Substitution.Marker == "" { + as.Substitution.Marker = "[MARKER]" + } + + found := false + for i := range fm.Substitutions { + s := fm.Substitutions[i] + if s.Name == as.Substitution.Name { + // update the substitution if we find it + found = true + fm.Substitutions[i] = as.Substitution + break + } + } + if !found { + // add the substitution if it wasn't found + fm.Substitutions = append(fm.Substitutions, as.Substitution) + } + if err := fm.Write(field); err != nil { + return errors.Wrap(err) + } + return nil +} diff --git a/cmd/config/internal/sub/dokio.go b/cmd/config/internal/sub/dokio.go new file mode 100644 index 000000000..794283a8b --- /dev/null +++ b/cmd/config/internal/sub/dokio.go @@ -0,0 +1,53 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package sub + +import ( + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +var _ kio.Filter = &PerformSubstitutions{} + +// Sub performs substitutions +type PerformSubstitutions struct { + // Name is the name of the substitution to perform + Name string + + // NewValue is the substitution value + NewValue string + + // Override if set to true will re-substitute already fields with a new value + Override bool + + // Revert if set to true will substitute fields back to the marker value + Revert bool + + // Description, if set will annotate the field with a description. + Description string + + // OwnedBy, if set will annotate the field with an owner. + OwnedBy string + + // Count is the number of substitutions performed by Filter. + Count int +} + +func (s *PerformSubstitutions) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) { + for i := range input { + p := &performSubstitutions{ + Name: s.Name, + Override: s.Override, + Revert: s.Revert, + NewValue: s.NewValue, + OwnedBy: s.OwnedBy, + Description: s.Description, + } + if err := input[i].PipeE(p); err != nil { + return nil, err + } + s.Count += p.Count + } + return input, nil +} diff --git a/cmd/config/internal/sub/doyaml.go b/cmd/config/internal/sub/doyaml.go new file mode 100644 index 000000000..19c2b5198 --- /dev/null +++ b/cmd/config/internal/sub/doyaml.go @@ -0,0 +1,155 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package sub substitutes strings in fields +package sub + +import ( + "strings" + + "sigs.k8s.io/kustomize/kyaml/fieldmeta" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +var _ yaml.Filter = &performSubstitutions{} + +// substituteResource substitutes a Marker value on a field +type performSubstitutions struct { + // Name of the substitution to perform. + Name string + + // Override if set to true will replace previously substituted values + Override bool + + // Revert if set to true will undo previously substituted values + Revert bool + + // NewValue is the new value to set. Mutually exclusive with Revert. + NewValue string + + // Description, if set will annotate the field with a description. + Description string + + // OwnedBy, if set will annotate the field with an owner. + OwnedBy string + + // Count will be incremented for each substituted value. + Count int +} + +// Filter performs the substitutions for a single object +func (fs *performSubstitutions) Filter(object *yaml.RNode) (*yaml.RNode, error) { + switch object.YNode().Kind { + case yaml.DocumentNode: + return fs.Filter(yaml.NewRNode(object.YNode().Content[0])) + case yaml.MappingNode: + return object, object.VisitFields(func(node *yaml.MapNode) error { + _, err := fs.Filter(node.Value) + return err + }) + case yaml.SequenceNode: + return object, object.VisitElements(func(node *yaml.RNode) error { + _, err := fs.Filter(node) + return err + }) + case yaml.ScalarNode: + s, f, err := fs.findSub(object) + if err != nil { + return nil, err + } + if s == nil { + return object, nil + } + return object, fs.substitute(object, s, f) + default: + return object, nil + } +} + +// findSub finds the substitution matching the name if one exists +func (fs *performSubstitutions) findSub(field *yaml.RNode) ( + *fieldmeta.Substitution, *fieldmeta.FieldMeta, error) { + // check if there are any substitutions for this field + var fm = &fieldmeta.FieldMeta{} + if err := fm.Read(field); err != nil { + return nil, nil, err + } + if fs.OwnedBy != "" { + fm.OwnedBy = fs.OwnedBy + } + if fs.Description != "" { + fm.Description = fs.Description + } + + // check if there is a matching substitution + for i := range fm.Substitutions { + if fm.Substitutions[i].Name == fs.Name { + // validate the value if we are not reverting to the marker. + // markers are allowed to be invalid. + // only validate if there is a substitution matching the name + if !fs.Revert { + if err := fm.Type.Validate(fs.NewValue); err != nil { + return nil, nil, err + } + } + return &fm.Substitutions[i], fm, nil + } + } + return nil, nil, nil +} + +// substitute performs the substitution for the given field, substitution, and metadata +func (fs *performSubstitutions) substitute( + field *yaml.RNode, s *fieldmeta.Substitution, f *fieldmeta.FieldMeta) error { + // undo or override previous substitutions by substituting the marker back + // NOTE: check if s.Value != "" so we never try to substitute the empty string back + if (fs.Revert || fs.Override) && s.Value != "" { + // revert to the marker value + if strings.Contains(field.YNode().Value, s.Value) { + // revert the substitution + field.YNode().Value = strings.ReplaceAll(field.YNode().Value, s.Value, s.Marker) + // only use the tag matching the type if the marker parses to that type + field.YNode().Tag = f.Type.TagForValue(s.Marker) + // record that the config has been modified + } + } + if fs.Revert { + fs.Count++ + s.Value = "" // value has been cleared and replaced with marker + if err := f.Write(field); err != nil { + return err + } + return nil + } + + if s.Value == fs.NewValue || !strings.Contains(field.YNode().Value, s.Marker) { + // no substitutions necessary -- already substituted or doesn't have the marker + return nil + } + + // replace the marker with the new value + field.YNode().Value = strings.ReplaceAll(field.YNode().Value, s.Marker, fs.NewValue) + // be sure to set the tag so the yaml doesn't incorrectly quote ints, bools or floats + field.YNode().Tag = f.Type.Tag() + field.YNode().Style = 0 + // record that the config has been modified + fs.Count++ + + // update the comment on the field + s.Value = fs.NewValue + if err := f.Write(field); err != nil { + return err + } + return nil +} diff --git a/cmd/config/internal/sub/lookupkio.go b/cmd/config/internal/sub/lookupkio.go new file mode 100644 index 000000000..76921c119 --- /dev/null +++ b/cmd/config/internal/sub/lookupkio.go @@ -0,0 +1,69 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package sub + +import ( + "sort" + + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +var _ kio.Filter = &LookupSubstitutions{} + +// Sub performs substitutions +type LookupSubstitutions struct { + // Name is the name of the substitution to match. If unspecified, all substitutions will + // be matched. + Name string + + // SubstitutionCounts are the aggregate substitutions matched. + SubstitutionCounts []FieldSubstitutionCount +} + +type FieldSubstitutionCount struct { + // Count is the number of substitutions possible to perform + Count int + + // CountComplete is the number of substitutions that have already been performed + // independent of this object. + CountComplete int + + // FieldSubstitution is the substitution found + FieldSubstitution +} + +func (l *LookupSubstitutions) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) { + subs := map[string]*FieldSubstitutionCount{} + for i := range input { + // lookup substitutions for this object + ls := &lookupSubstitutions{Name: l.Name} + if err := input[i].PipeE(ls); err != nil { + return nil, err + } + + // aggregate counts for each substitution + for j := range ls.Substitutions { + sub := ls.Substitutions[j] + curr, found := subs[sub.Name] + if !found { + curr = &FieldSubstitutionCount{FieldSubstitution: sub} + subs[sub.Name] = curr + } + curr.Count++ + if sub.CurrentValue != "" { + curr.CountComplete++ + } + } + } + + // pull out and sort the results + for _, v := range subs { + l.SubstitutionCounts = append(l.SubstitutionCounts, *v) + } + sort.Slice(l.SubstitutionCounts, func(i, j int) bool { + return l.SubstitutionCounts[i].Name < l.SubstitutionCounts[j].Name + }) + return input, nil +} diff --git a/cmd/config/internal/sub/lookupyaml.go b/cmd/config/internal/sub/lookupyaml.go new file mode 100644 index 000000000..dee2abf29 --- /dev/null +++ b/cmd/config/internal/sub/lookupyaml.go @@ -0,0 +1,65 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package sub + +import ( + "sigs.k8s.io/kustomize/kyaml/fieldmeta" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +var _ yaml.Filter = &lookupSubstitutions{} + +// substituteResource substitutes a Marker value on a field +type lookupSubstitutions struct { + // Name of the substitution to lookup. If unspecified lookup all substitutions. + Name string + + // FieldSubstitution is the list of substitutions that were found + Substitutions []FieldSubstitution +} + +func (ls *lookupSubstitutions) Filter(object *yaml.RNode) (*yaml.RNode, error) { + switch object.YNode().Kind { + case yaml.DocumentNode: + return ls.Filter(yaml.NewRNode(object.YNode().Content[0])) + case yaml.MappingNode: + return object, object.VisitFields(func(node *yaml.MapNode) error { + _, err := ls.Filter(node.Value) + return err + }) + case yaml.SequenceNode: + return object, object.VisitElements(func(node *yaml.RNode) error { + _, err := ls.Filter(node) + return err + }) + case yaml.ScalarNode: + return object, ls.lookup(object) + default: + return object, nil + } +} + +// lookup finds any substitutions for this field +func (ls *lookupSubstitutions) lookup(field *yaml.RNode) error { + // check if there is a substitution for this field + var fm = &fieldmeta.FieldMeta{} + if err := fm.Read(field); err != nil { + return err + } + + for i := range fm.Substitutions { + s := fm.Substitutions[i] + if ls.Name == "" || ls.Name == s.Name { + ls.Substitutions = append(ls.Substitutions, FieldSubstitution{ + Name: s.Name, + CurrentValue: s.Value, + Description: fm.Description, + Marker: s.Marker, + Type: fm.Type, + OwnedBy: fm.OwnedBy, + }) + } + } + return nil +} diff --git a/cmd/config/internal/sub/types.go b/cmd/config/internal/sub/types.go new file mode 100644 index 000000000..f8b6a86b7 --- /dev/null +++ b/cmd/config/internal/sub/types.go @@ -0,0 +1,29 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package sub + +import ( + "sigs.k8s.io/kustomize/kyaml/fieldmeta" +) + +// FieldSubstitution is a possible field substitution read from a field +type FieldSubstitution struct { + // Name is the name of the substitution + Name string + + // Description is a description of the fields current value + Description string + + // Value is the current substituted value for the field. + CurrentValue string + + // Type is the type of the substitution + Type fieldmeta.FieldValueType + + // Marker is the marker used + Marker string + + // OwnedBy, if set will annotate the field with an owner. + OwnedBy string +} diff --git a/kustomize/go.sum b/kustomize/go.sum index 0f4a2732d..02dc9555c 100644 --- a/kustomize/go.sum +++ b/kustomize/go.sum @@ -240,6 +240,8 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7 github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -261,6 +263,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= diff --git a/kyaml/fieldmeta/fieldmeta.go b/kyaml/fieldmeta/fieldmeta.go new file mode 100644 index 000000000..7b47229db --- /dev/null +++ b/kyaml/fieldmeta/fieldmeta.go @@ -0,0 +1,134 @@ +package fieldmeta + +import ( + "bytes" + "encoding/json" + "strconv" + "strings" + + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// FieldMeta contains metadata that may be attached to fields as comments +type FieldMeta struct { + // Substitutions are substitutions that may be performed against this field + Substitutions []Substitution `yaml:"substitutions,omitempty" json:"substitutions,omitempty"` + // OwnedBy records the owner of this field + OwnedBy string `yaml:"ownedBy,omitempty" json:"ownedBy,omitempty"` + // DefaultedBy records that this field was default, but may be changed by other owners + DefaultedBy string `yaml:"defaultedBy,omitempty" json:"defaultedBy,omitempty"` + // Description is a description of the current field value, e.g. why it was set + Description string `yaml:"description,omitempty" json:"description,omitempty"` + // Type is the type of the field value + Type FieldValueType `yaml:"type,omitempty" json:"type,omitempty"` +} + +// Substitution defines a substitution that may be performed against the field +type Substitution struct { + // Name is the name of the substitution and read by tools + Name string `yaml:"name,omitempty" json:"name,omitempty"` + // Marker is the marker used for replacement + Marker string `yaml:"marker,omitempty" json:"marker,omitempty"` + // Value is the current value that has been substituted for the Marker + Value string `yaml:"value,omitempty" json:"value,omitempty"` +} + +// Read reads the FieldMeta from a node +func (fm *FieldMeta) Read(n *yaml.RNode) error { + if n.YNode().LineComment != "" { + v := strings.TrimLeft(n.YNode().LineComment, "#") + // if it doesn't Unmarshal that is fine, it means there is no metadata + // other comments are valid, they just don't parse + d := yaml.NewDecoder(bytes.NewBuffer([]byte(v))) + d.KnownFields(false) + _ = d.Decode(fm) + } + return nil +} + +// Write writes the FieldMeta to a node +func (fm *FieldMeta) Write(n *yaml.RNode) error { + b, err := json.Marshal(fm) + if err != nil { + return err + } + n.YNode().LineComment = string(b) + return nil +} + +// FieldValueType defines the type of input to register +type FieldValueType string + +const ( + // String defines a string flag + String FieldValueType = "string" + // Bool defines a bool flag + Bool = "bool" + // Float defines a float flag + Float = "float" + // Int defines an int flag + Int = "int" +) + +func (it FieldValueType) String() string { + if it == "" { + return "string" + } + return string(it) +} + +func (it FieldValueType) Validate(value string) error { + switch it { + case Int: + if _, err := strconv.Atoi(value); err != nil { + return errors.WrapPrefixf(err, "value must be an int") + } + case Bool: + if _, err := strconv.ParseBool(value); err != nil { + return errors.WrapPrefixf(err, "value must be a bool") + } + case Float: + if _, err := strconv.ParseFloat(value, 64); err != nil { + return errors.WrapPrefixf(err, "value must be a float") + } + } + return nil +} + +func (it FieldValueType) Tag() string { + switch it { + case String: + return "!!str" + case Bool: + return "!!bool" + case Int: + return "!!int" + case Float: + return "!!float" + } + return "" +} + +func (it FieldValueType) TagForValue(value string) string { + switch it { + case String: + return "!!str" + case Bool: + if _, err := strconv.ParseBool(string(it)); err != nil { + return "" + } + return "!!bool" + case Int: + if _, err := strconv.ParseInt(string(it), 0, 32); err != nil { + return "" + } + return "!!int" + case Float: + if _, err := strconv.ParseFloat(string(it), 64); err != nil { + return "" + } + return "!!float" + } + return "" +}