cmd/config set: Support for setting fields imperatively from the cli

This commit is contained in:
Phillip Wittrock
2019-12-15 14:57:47 -08:00
parent 7c8e2f3948
commit 62e969c719
17 changed files with 1437 additions and 0 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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=

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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=

View File

@@ -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 ""
}