refactor: Remove setters from kyaml

Kind:

Refactor

Summary:

Setters functionality is provided as a KRM function. We should remove code related to setters in cmd/config and kyaml.
As of now most setters2 and setters usage are related to fork of kpt, however, these:
[fluxcd/image-automation-controller](6827808a1a/pkg/update/filter.go (L24)) with [kyml](6827808a1a/go.mod (L42))
[rancher/fleet](0a6cf6cb92/internal/cmd/controller/controllers/image/update/setters.go (L16)) with [kyaml](0a6cf6cb92/go.mod (L75))

Repositories still using them, They pinned these two into a specific kyaml version. If we decide to go for this removal then we can make a release note that this is actually removed on the next version since we already marked this as deprecated before.
This commit is contained in:
Irvi Aini
2023-08-24 00:39:47 +08:00
parent cd9a16cfab
commit 4947a905fa
27 changed files with 11 additions and 6374 deletions

View File

@@ -5,32 +5,39 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/dustmop/soup v1.1.2-0.20190516214245-38228baa104e/go.mod h1:CgNC6SGbT+Xb8wGGvzilttZL1mc5sQ/5KkcxsZttMIk=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw=
github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.starlark.net v0.0.0-20190528202925-30ae18b8564f/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -39,6 +46,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=

View File

@@ -1,74 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package fixsetters
import (
"encoding/json"
"fmt"
"strings"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// FieldMeta contains metadata that may be attached to fields as comments
type FieldMetaV1 struct {
Schema spec.Schema
Extensions XKustomize
}
type XKustomize struct {
SetBy string `yaml:"setBy,omitempty" json:"setBy,omitempty"`
PartialFieldSetters []PartialFieldSetter `yaml:"partialSetters,omitempty" json:"partialSetters,omitempty"`
FieldSetter *PartialFieldSetter `yaml:"setter,omitempty" json:"setter,omitempty"`
}
// PartialFieldSetter defines how to set part of a field rather than the full field
// value. e.g. the tag part of an image field
type PartialFieldSetter struct {
// Name is the name of this setter.
Name string `yaml:"name" json:"name"`
// Value is the current value that has been set.
Value string `yaml:"value" json:"value"`
}
// UpgradeV1SetterComment reads the FieldMeta from a node and upgrade the
// setters comment to latest
func (fm *FieldMetaV1) UpgradeV1SetterComment(n *yaml.RNode) error {
// check for metadata on head and line comments
comments := []string{n.YNode().LineComment, n.YNode().HeadComment}
for _, c := range comments {
if c == "" {
continue
}
c := strings.TrimLeft(c, "#")
if err := fm.Schema.UnmarshalJSON([]byte(c)); err != nil {
// note: don't return an error if the comment isn't a fieldmeta struct
return nil
}
fe := fm.Schema.VendorExtensible.Extensions["x-kustomize"]
if fe == nil {
return nil
}
b, err := json.Marshal(fe)
if err != nil {
return errors.Wrap(err)
}
// delete line comment after parsing info into fieldmeta
n.YNode().HeadComment = ""
n.YNode().LineComment = ""
err = json.Unmarshal(b, &fm.Extensions)
if fm.Extensions.FieldSetter != nil {
n.YNode().LineComment = fmt.Sprintf(`{"%s":"%s"}`, fieldmeta.ShortHandRef(), fm.Extensions.FieldSetter.Name)
}
return err
}
return nil
}

View File

@@ -1,141 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package fixsetters
import (
"os"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/setters2"
"sigs.k8s.io/kustomize/kyaml/setters2/settersutil"
)
// SetterFixer fixes setters in the input package
type SetterFixer struct {
// PkgPath is path to the resource package
PkgPath string
// OpenAPIPath is path to the openAPI file in the package
OpenAPIPath string
// DryRun only displays the actions without performing
DryRun bool
}
// SetterFixerV1Result holds the results of V1 setters fix
type SetterFixerV1Result struct {
// NeedFix indicates if the resource in pkgPath are on V1 version of setters
// and need to be fixed
NeedFix bool
// CreatedSetters are setters created as part of this fix
CreatedSetters []string
// CreatedSubst are substitutions created as part of this fix
CreatedSubst []string
// FailedSetters are setters failed to create from current V1 setters
FailedSetters map[string]error
// FailedSubst are substitutions failed to be created from V1 partial setters
FailedSubst map[string]error
}
// FixSettersV1 reads the package and upgrades v1 version of setters
// to latest
func (f *SetterFixer) FixV1Setters() (SetterFixerV1Result, error) {
sfr := SetterFixerV1Result{
FailedSetters: make(map[string]error),
FailedSubst: make(map[string]error),
}
// KrmFile need not exist for dryRun
if !f.DryRun {
_, err := os.Stat(f.OpenAPIPath)
if err != nil {
return sfr, err
}
}
var err error
// lookup for all setters and partial setters in v1 format
// delete the v1 format comments after lookup
l := UpgradeV1Setters{}
if f.DryRun {
err = applyReadFilter(&l, f.PkgPath)
} else {
err = applyWriteFilter(&l, f.PkgPath)
}
if err != nil {
return sfr, err
}
if len(l.SetterCounts) > 0 {
sfr.NeedFix = true
} else {
return sfr, nil
}
// for each v1 setter create the equivalent in v2,
for _, setter := range l.SetterCounts {
sd := setters2.SetterDefinition{
Name: setter.Name,
Value: setter.Value,
Description: setter.Description,
SetBy: setter.SetBy,
Type: setter.Type,
}
var err error
if !f.DryRun {
err = sd.AddToFile(f.OpenAPIPath)
}
if err != nil {
sfr.FailedSetters[setter.Name] = err
} else {
sfr.CreatedSetters = append(sfr.CreatedSetters, setter.Name)
}
}
// for each group of partial setters, create equivalent substitution
for _, subst := range l.Substitutions {
sc := settersutil.SubstitutionCreator{
Name: subst.Name,
FieldValue: subst.FieldVale,
Pattern: subst.Pattern,
ResourcesPath: f.PkgPath,
OpenAPIPath: f.OpenAPIPath,
}
var err error
if !f.DryRun {
err = applyWriteFilter(&sc, f.PkgPath)
}
if err != nil {
sfr.FailedSubst[subst.Name] = err
} else {
sfr.CreatedSubst = append(sfr.CreatedSubst, subst.Name)
}
}
return sfr, nil
}
func applyWriteFilter(f kio.Filter, pkgPath string) error {
rw := &kio.LocalPackageReadWriter{
PackagePath: pkgPath,
}
return kio.Pipeline{
Inputs: []kio.Reader{rw},
Filters: []kio.Filter{f},
Outputs: []kio.Writer{rw},
}.Execute()
}
func applyReadFilter(f kio.Filter, pkgPath string) error {
rw := &kio.LocalPackageReader{
PackagePath: pkgPath,
}
return kio.Pipeline{
Inputs: []kio.Reader{rw},
Filters: []kio.Filter{f},
}.Execute()
}

View File

@@ -1,460 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package fixsetters
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFixSettersV1(t *testing.T) {
var tests = []struct {
name string
input string
err string
dryRun bool
openAPIFile string
expectedOutput string
expectedOpenAPI string
needFix bool
createdSetters []string
createdSubst []string
failedSetters map[string]error
failedSubst map[string]error
}{
{
name: "upgrade-delete-partial-setters",
input: `
apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
cluster: "someproj/someclus" # {"type":"string","x-kustomize":{"partialSetters":[{"name":"project","value":"someproj"},{"name":"cluster","value":"someclus"}]}}
spec:
profile: asm # {"type":"string","x-kustomize":{"setter":{"name":"profile","value":"asm"}}}
cluster: "someproj/someclus" # {"type":"string","x-kustomize":{"partialSetters":[{"name":"project","value":"someproj"},{"name":"cluster","value":"someclus"}]}}
`,
openAPIFile: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization`,
needFix: true,
createdSetters: []string{"cluster", "profile", "project"},
createdSubst: []string{"project-cluster-54235872"},
failedSetters: map[string]error{},
failedSubst: map[string]error{},
expectedOutput: `apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
cluster: "someproj/someclus" # {"$openapi":"project-cluster-54235872"}
spec:
profile: asm # {"$openapi":"profile"}
cluster: "someproj/someclus" # {"$openapi":"project-cluster-54235872"}
`,
expectedOpenAPI: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization
openAPI:
definitions:
io.k8s.cli.setters.cluster:
type: string
x-k8s-cli:
setter:
name: cluster
value: someclus
io.k8s.cli.setters.profile:
type: string
x-k8s-cli:
setter:
name: profile
value: asm
io.k8s.cli.setters.project:
type: string
x-k8s-cli:
setter:
name: project
value: someproj
io.k8s.cli.substitutions.project-cluster-54235872:
x-k8s-cli:
substitution:
name: project-cluster-54235872
pattern: ${project}/${cluster}
values:
- marker: ${project}
ref: '#/definitions/io.k8s.cli.setters.project'
- marker: ${cluster}
ref: '#/definitions/io.k8s.cli.setters.cluster'
`,
},
{
name: "upgrade-delete-partial-setters-dryRun",
dryRun: true,
input: `apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
cluster: "someproj/someclus" # {"type":"string","x-kustomize":{"partialSetters":[{"name":"project","value":"someproj"},{"name":"cluster","value":"someclus"}]}}
spec:
profile: asm # {"type":"string","x-kustomize":{"setter":{"name":"profile","value":"asm"}}}
cluster: "someproj/someclus" # {"type":"string","x-kustomize":{"partialSetters":[{"name":"project","value":"someproj"},{"name":"cluster","value":"someclus"}]}}
`,
needFix: true,
createdSetters: []string{"cluster", "profile", "project"},
createdSubst: []string{"project-cluster-54235872"},
failedSetters: map[string]error{},
failedSubst: map[string]error{},
expectedOutput: `apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
cluster: "someproj/someclus" # {"type":"string","x-kustomize":{"partialSetters":[{"name":"project","value":"someproj"},{"name":"cluster","value":"someclus"}]}}
spec:
profile: asm # {"type":"string","x-kustomize":{"setter":{"name":"profile","value":"asm"}}}
cluster: "someproj/someclus" # {"type":"string","x-kustomize":{"partialSetters":[{"name":"project","value":"someproj"},{"name":"cluster","value":"someclus"}]}}
`,
},
{
name: "partial-setters-same-value",
input: `
apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
spec:
profile: asm # {"type":"string","x-kustomize":{"setter":{"name":"profile","value":"asm"}}}
team: asm # {"type":"string","x-kustomize":{"setter":{"name":"team","value":"asm"}}}
profile-team: asm/asm # {"type":"string","x-kustomize":{"partialSetters":[{"name":"profile","value":"asm"},{"name":"team","value":"asm"}]}}
`,
openAPIFile: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization`,
needFix: true,
createdSetters: []string{"profile", "team"},
createdSubst: []string{"profile-team-1851878264"},
failedSetters: map[string]error{},
failedSubst: map[string]error{},
expectedOutput: `apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
spec:
profile: asm # {"$openapi":"profile"}
team: asm # {"$openapi":"team"}
profile-team: asm/asm # {"$openapi":"profile-team-1851878264"}
`,
expectedOpenAPI: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization
openAPI:
definitions:
io.k8s.cli.setters.profile:
type: string
x-k8s-cli:
setter:
name: profile
value: asm
io.k8s.cli.setters.team:
type: string
x-k8s-cli:
setter:
name: team
value: asm
io.k8s.cli.substitutions.profile-team-1851878264:
x-k8s-cli:
substitution:
name: profile-team-1851878264
pattern: ${profile}/${team}
values:
- marker: ${profile}
ref: '#/definitions/io.k8s.cli.setters.profile'
- marker: ${team}
ref: '#/definitions/io.k8s.cli.setters.team'
`,
},
{
name: "partial-setters-suffix-subst",
input: `
apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
spec:
profile: asm-profile # {"type":"string","x-kustomize":{"partialSetters":[{"name":"asm","value":"asm"}]}}
team: asm-team # {"type":"string","x-kustomize":{"partialSetters":[{"name":"asm","value":"asm"}]}}
`,
openAPIFile: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization`,
needFix: true,
createdSetters: []string{"asm"},
createdSubst: []string{"asm-3472570278", "asm-3647054792"},
failedSetters: map[string]error{},
failedSubst: map[string]error{},
expectedOutput: `apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
spec:
profile: asm-profile # {"$openapi":"asm-3647054792"}
team: asm-team # {"$openapi":"asm-3472570278"}
`,
expectedOpenAPI: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization
openAPI:
definitions:
io.k8s.cli.setters.asm:
type: string
x-k8s-cli:
setter:
name: asm
value: asm
io.k8s.cli.substitutions.asm-3472570278:
x-k8s-cli:
substitution:
name: asm-3472570278
pattern: ${asm}-team
values:
- marker: ${asm}
ref: '#/definitions/io.k8s.cli.setters.asm'
io.k8s.cli.substitutions.asm-3647054792:
x-k8s-cli:
substitution:
name: asm-3647054792
pattern: ${asm}-profile
values:
- marker: ${asm}
ref: '#/definitions/io.k8s.cli.setters.asm'
`,
},
{
name: "upgrade-with-both-versions",
input: `
apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
clusterName: "project-id/us-east1-d/cluster-name"
spec:
profile: asm # {"type":"string","x-kustomize":{"setter":{"name":"profilesetter","value":"asm"}}}
hub: gcr.io/asm-testing # {"$openapi":"hubsetter"}
`,
openAPIFile: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization
openAPI:
definitions:
io.k8s.cli.setters.hubsetter:
type: string
x-k8s-cli:
setter:
name: hubsetter
value: gcr.io/asm-testing`,
needFix: true,
createdSetters: []string{"profilesetter"},
failedSetters: map[string]error{},
failedSubst: map[string]error{},
expectedOutput: `apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
clusterName: "project-id/us-east1-d/cluster-name"
spec:
profile: asm # {"$openapi":"profilesetter"}
hub: gcr.io/asm-testing # {"$openapi":"hubsetter"}
`,
expectedOpenAPI: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization
openAPI:
definitions:
io.k8s.cli.setters.hubsetter:
type: string
x-k8s-cli:
setter:
name: hubsetter
value: gcr.io/asm-testing
io.k8s.cli.setters.profilesetter:
type: string
x-k8s-cli:
setter:
name: profilesetter
value: asm
`,
},
{
name: "setter-already-exists",
input: `
apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
clusterName: "project-id/us-east1-d/cluster-name"
spec:
profile: asm # {"type":"string","x-kustomize":{"setter":{"name":"profilesetter","value":"asm"}}}
hub: asm # {"$openapi":"profilesetter"}
`,
openAPIFile: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization
openAPI:
definitions:
io.k8s.cli.setters.profilesetter:
type: string
x-k8s-cli:
setter:
name: profilesetter
value: asm
`,
needFix: true,
createdSetters: []string{"profilesetter"},
failedSetters: map[string]error{},
failedSubst: map[string]error{},
expectedOutput: `apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
clusterName: "project-id/us-east1-d/cluster-name"
spec:
profile: asm # {"$openapi":"profilesetter"}
hub: asm # {"$openapi":"profilesetter"}
`,
expectedOpenAPI: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization
openAPI:
definitions:
io.k8s.cli.setters.profilesetter:
type: string
x-k8s-cli:
setter:
name: profilesetter
value: asm
`,
},
{
name: "do-not-delete-latest setters",
input: `
apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
clusterName: "project-id/us-east1-d/cluster-name"
spec:
profile: asm # {"$openapi":"profilesetter"}
hub: gcr.io/asm-testing
`,
failedSetters: map[string]error{},
failedSubst: map[string]error{},
openAPIFile: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization
openAPI:
definitions:
io.k8s.cli.setters.profilesetter:
type: string
x-k8s-cli:
setter:
name: profilesetter
value: asm
`,
expectedOutput: `apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
clusterName: "project-id/us-east1-d/cluster-name"
spec:
profile: asm # {"$openapi":"profilesetter"}
hub: gcr.io/asm-testing
`,
expectedOpenAPI: `apiVersion: kustomization.dev/v1alpha1
kind: Kustomization
openAPI:
definitions:
io.k8s.cli.setters.profilesetter:
type: string
x-k8s-cli:
setter:
name: profilesetter
value: asm
`,
},
{
name: "no-openAPI-file-error",
input: `
apiVersion: install.istio.io/v1alpha2
kind: IstioControlPlane
metadata:
clusterName: "project-id/us-east1-d/cluster-name"
spec:
profile: asm # {"type":"string","x-kustomize":{"setter":{"name":"profilesetter","value":"asm"}}}
hub: gcr.io/asm-testing
`,
err: "Krmfile:",
},
}
for i := range tests {
test := tests[i]
t.Run(test.name, func(t *testing.T) {
openAPIFileName := "Krmfile"
dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "deploy.yaml"), []byte(test.input), 0600)
if !assert.NoError(t, err) {
t.FailNow()
}
if test.openAPIFile != "" {
err = os.WriteFile(filepath.Join(dir, openAPIFileName), []byte(test.openAPIFile), 0600)
if !assert.NoError(t, err) {
t.FailNow()
}
}
sf := SetterFixer{
PkgPath: dir,
DryRun: test.dryRun,
OpenAPIPath: filepath.Join(dir, "Krmfile"),
}
sfr, err := sf.FixV1Setters()
if test.err == "" {
if !assert.NoError(t, err) {
t.FailNow()
}
} else {
if !assert.Contains(t, err.Error(), test.err) {
t.FailNow()
}
return
}
actualOutput, err := os.ReadFile(filepath.Join(dir, "deploy.yaml"))
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, test.expectedOutput, string(actualOutput))
if test.expectedOpenAPI != "" {
actualOpenAPI, err := os.ReadFile(filepath.Join(dir, openAPIFileName))
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, test.expectedOpenAPI, string(actualOpenAPI))
}
assert.Equal(t, test.needFix, sfr.NeedFix)
assert.Equal(t, test.createdSetters, sfr.CreatedSetters)
assert.Equal(t, test.createdSubst, sfr.CreatedSubst)
assert.Equal(t, test.failedSubst, sfr.FailedSubst)
assert.Equal(t, test.failedSetters, sfr.FailedSetters)
})
}
}

View File

@@ -1,134 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package fixsetters
import (
"fmt"
"hash/fnv"
"strings"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
var _ yaml.Filter = &upgradeV1Setters{}
// upgradeV1Setters looks up v1 setters in a Resource and upgrades
// all the setters related comments
type upgradeV1Setters struct {
// Name of the setter to lookup. Optional
Name string
// Setters is a list of setters that were found
Setters []setter
// Substitutions is a list of substitutions that were found
Substitutions []substitution
}
type substitution struct {
Name string
FieldVale string
Pattern string
}
type setter struct {
PartialFieldSetter
Description string
Type string
SetBy string
SubstName string
}
func (ls *upgradeV1Setters) Filter(object *yaml.RNode) (*yaml.RNode, error) {
switch object.YNode().Kind {
case yaml.DocumentNode:
// skip the document node
return ls.Filter(yaml.NewRNode(object.YNode().Content[0]))
case yaml.MappingNode:
return object, object.VisitFields(func(node *yaml.MapNode) error {
return node.Value.PipeE(ls)
})
case yaml.SequenceNode:
return object, object.VisitElements(func(node *yaml.RNode) error {
return node.PipeE(ls)
})
case yaml.ScalarNode:
return object, ls.lookupAndUpgrade(object)
default:
return object, nil
}
}
// lookupAndUpgrade finds any setters for a field and upgrades the setters comment
func (ls *upgradeV1Setters) lookupAndUpgrade(field *yaml.RNode) error {
// check if there is a substitution for this field
var fm = FieldMetaV1{}
if err := fm.UpgradeV1SetterComment(field); err != nil {
return err
}
if fm.Extensions.FieldSetter != nil {
if ls.Name != "" && ls.Name != fm.Extensions.FieldSetter.Name {
// skip this setter, it doesn't match the specified setter
return nil
}
// full setter
ls.Setters = append(ls.Setters, setter{
PartialFieldSetter: *fm.Extensions.FieldSetter,
Description: fm.Schema.Description,
Type: fm.Schema.Type[0],
SetBy: fm.Extensions.SetBy,
})
return nil
}
if len(fm.Extensions.PartialFieldSetters) > 0 {
fieldValue := field.YNode().Value
pattern := fieldValue
var substName string
// derive substitution pattern from partial setters
for i := range fm.Extensions.PartialFieldSetters {
substName += fm.Extensions.PartialFieldSetters[i].Name + "-"
pattern = strings.Replace(pattern, fm.Extensions.PartialFieldSetters[i].Value, `${`+fm.Extensions.PartialFieldSetters[i].Name+"}", 1)
}
fvHash, err := FNV32aHash(fieldValue)
if err != nil {
return err
}
substName += fvHash
ls.Substitutions = append(ls.Substitutions, substitution{
Name: substName,
FieldVale: fieldValue,
Pattern: pattern,
})
}
for i := range fm.Extensions.PartialFieldSetters {
if ls.Name != "" && ls.Name != fm.Extensions.PartialFieldSetters[i].Name {
// skip this setter
continue
}
ls.Setters = append(ls.Setters, setter{
PartialFieldSetter: fm.Extensions.PartialFieldSetters[i],
Description: fm.Schema.Description,
Type: fm.Schema.Type[0],
SetBy: fm.Extensions.SetBy,
})
}
return nil
}
// FNV32aHash generates 32-bit FNV-1a hash for input string
func FNV32aHash(text string) (string, error) {
algorithm := fnv.New32a()
_, err := algorithm.Write([]byte(text))
if err != nil {
return "", errors.Wrap(err)
}
return fmt.Sprint(algorithm.Sum32()), nil
}

View File

@@ -1,86 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package fixsetters
import (
"sort"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
var _ kio.Filter = &UpgradeV1Setters{}
// UpgradeV1Setters identifies setters for a collection of Resources to upgrade
type UpgradeV1Setters struct {
// Name is the name of the setter to match. Optional.
Name string
// SetterCounts is populated by Filter and contains the count of fields matching each setter.
SetterCounts []setterCount
// Substitutions are groups of partial setters
Substitutions []substitution
}
// setterCount records the identified setters and number of fields matching those setters
type setterCount struct {
// Count is the number of substitutions possible to perform
Count int
// setter is the substitution found
setter
}
// Filter implements kio.Filter
func (l *UpgradeV1Setters) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
setters := map[string]*setterCount{}
substitutions := map[string]*substitution{}
for i := range input {
// lookup substitutions for this object
ls := &upgradeV1Setters{Name: l.Name}
if err := input[i].PipeE(ls); err != nil {
return nil, err
}
// aggregate counts for each setter by name. takes the description and value from
// the first setter for each name encountered.
for j := range ls.Setters {
setter := ls.Setters[j]
curr, found := setters[setter.Name]
if !found {
curr = &setterCount{setter: setter}
setters[setter.Name] = curr
}
curr.Count++
}
for j := range ls.Substitutions {
subst := ls.Substitutions[j]
_, found := substitutions[subst.Name]
if !found {
substitutions[subst.Name] = &subst
}
}
}
// pull out and sort the results by setter name
for _, v := range setters {
l.SetterCounts = append(l.SetterCounts, *v)
}
for _, subst := range substitutions {
l.Substitutions = append(l.Substitutions, *subst)
}
sort.Slice(l.Substitutions, func(i, j int) bool {
return l.Substitutions[i].Name < l.Substitutions[j].Name
})
sort.Slice(l.SetterCounts, func(i, j int) bool {
return l.SetterCounts[i].Name < l.SetterCounts[j].Name
})
return input, nil
}

View File

@@ -1,302 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"reflect"
"strings"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// Add creates or updates setter or substitution references from resource fields.
// Requires that at least one of FieldValue and FieldName have been set.
type Add struct {
// FieldValue if set will add the OpenAPI reference to fields if they have this value.
// Optional. If unspecified match all field values.
FieldValue string
// FieldName if set will add the OpenAPI reference to fields with this name or path
// FieldName may be the full name of the field, full path to the field, or the path suffix.
// e.g. all of the following would match spec.template.spec.containers.image --
// [image, containers.image, spec.containers.image, template.spec.containers.image,
// spec.template.spec.containers.image]
// Optional. If unspecified match all field names.
FieldName string
// Ref is the OpenAPI reference to set on the matching fields as a comment.
Ref string
// ListValues are the value of a list setter.
ListValues []string
// Type is the type of the setter value
Type string
// Count is the number of fields the setter applies to
Count int
SettersSchema *spec.Schema
}
// Filter implements yaml.Filter
func (a *Add) Filter(object *yaml.RNode) (*yaml.RNode, error) {
if a.FieldName == "" && a.FieldValue == "" {
return nil, errors.Errorf("must specify either fieldName or fieldValue")
}
if a.Ref == "" {
return nil, errors.Errorf("must specify ref")
}
return object, accept(a, object, a.SettersSchema)
}
func (a *Add) visitSequence(_ *yaml.RNode, _ string, _ *openapi.ResourceSchema) error {
// no-op
return nil
}
// visitMapping implements visitor
// visitMapping visits the fields in input MappingNode and adds setter/subst ref
// if the path path spec matches with input FiledName
func (a *Add) visitMapping(object *yaml.RNode, p string, _ *openapi.ResourceSchema) error {
return object.VisitFields(func(node *yaml.MapNode) error {
if node.Value.YNode().Kind != yaml.SequenceNode {
return nil
}
key, err := node.Key.String()
if err != nil {
return err
}
// derive the list values for the sequence node to write it to openAPI definitions
var values []string
for _, sc := range node.Value.Content() {
values = append(values, sc.Value)
}
// pathToKey refers to the path address of the key node ex: metadata.annotations
// p is the path till parent node, pathToKey is obtained by appending child key
pathToKey := p + "." + strings.Trim(key, "\n")
if a.FieldName != "" && strings.HasSuffix(pathToKey, a.FieldName) {
// check if there are different values for field path before adding ref to the field
if len(a.ListValues) > 0 && !reflect.DeepEqual(values, a.ListValues) {
return errors.Errorf("setters can only be created for fields with same values, "+
"encountered different array values for specified field path: %s, %s", values, a.ListValues)
}
a.ListValues = values
a.Count++
return a.addRef(node.Key)
}
return nil
})
}
// visitScalar implements visitor
// visitScalar will set the field metadata on each scalar field whose name + value match
func (a *Add) visitScalar(object *yaml.RNode, p string, _, _ *openapi.ResourceSchema) error {
// check if the field matches
if a.Type == "array" {
return nil
}
if a.FieldName != "" && !strings.HasSuffix(p, a.FieldName) {
return nil
}
if a.FieldValue != "" && a.FieldValue != object.YNode().Value {
return nil
}
a.Count++
return a.addRef(object)
}
// addRef adds the setter/subst ref to the object node as a line comment
func (a *Add) addRef(object *yaml.RNode) error {
// read the field metadata
fm := fieldmeta.FieldMeta{SettersSchema: a.SettersSchema}
if err := fm.Read(object); err != nil {
return err
}
// create the ref on the field metadata
r, err := spec.NewRef(a.Ref)
if err != nil {
return err
}
fm.Schema.Ref = r
// write the field metadata
if err := fm.Write(object); err != nil {
return err
}
return nil
}
// SetterDefinition may be used to update a files OpenAPI definitions with a new setter.
type SetterDefinition struct {
// Name is the name of the setter to create or update.
Name string `yaml:"name"`
// Value is the value of the setter.
Value string `yaml:"value"`
// ListValues are the value of a list setter.
ListValues []string `yaml:"listValues,omitempty"`
// SetBy is the person or role that last set the value.
SetBy string `yaml:"setBy,omitempty"`
// Description is a description of the value.
Description string `yaml:"description,omitempty"`
// Count is the number of fields set by this setter.
Count int `yaml:"count,omitempty"`
// Type is the type of the setter value.
Type string `yaml:"type,omitempty"`
// Schema is the openAPI schema for setter constraints.
Schema string `yaml:"schema,omitempty"`
// EnumValues is a map of possible setter values to actual field values.
// If EnumValues is specified, then the value set the by user 1) MUST
// be present in the enumValues map as a key, and 2) the map entry value
// MUST be used as the value to set in the configuration (rather than the key)
// Example -- may be used for t-shirt sizing values by allowing cpu to be
// set to small, medium or large, and then mapping these values to cpu values -- 0.5, 2, 8
EnumValues map[string]string `yaml:"enumValues,omitempty"`
// Required indicates that the setter must be set by package consumer before
// live apply/preview. This field is added to the setter definition to record
// the package publisher's intent to make the setter required to be set.
Required bool `yaml:"required,omitempty"`
// IsSet indicates the specified field has been explicitly assigned.
IsSet bool `yaml:"isSet,omitempty"`
}
func (sd SetterDefinition) AddToFile(path string) error {
return yaml.UpdateFile(sd, path)
}
func (sd SetterDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) {
key := fieldmeta.SetterDefinitionPrefix + sd.Name
definitions, err := object.Pipe(yaml.LookupCreate(
yaml.MappingNode, openapi.SupplementaryOpenAPIFieldName, "definitions"))
if err != nil {
return nil, err
}
setterDef, err := definitions.Pipe(yaml.LookupCreate(yaml.MappingNode, key))
if err != nil {
return nil, err
}
if sd.Schema != "" {
schNode, err := yaml.ConvertJSONToYamlNode(sd.Schema)
if err != nil {
return nil, err
}
err = definitions.PipeE(yaml.SetField(key, schNode))
if err != nil {
return nil, err
}
// don't write the schema to the extension
sd.Schema = ""
}
if sd.Description != "" {
err = setterDef.PipeE(yaml.FieldSetter{Name: "description", StringValue: sd.Description})
if err != nil {
return nil, err
}
// don't write the description to the extension
sd.Description = ""
}
if sd.Type != "" {
err = setterDef.PipeE(yaml.FieldSetter{Name: "type", StringValue: sd.Type})
if err != nil {
return nil, err
}
// don't write the type to the extension
sd.Type = ""
}
ext, err := setterDef.Pipe(yaml.LookupCreate(yaml.MappingNode, K8sCliExtensionKey))
if err != nil {
return nil, err
}
b, err := yaml.Marshal(sd)
if err != nil {
return nil, err
}
y, err := yaml.Parse(string(b))
if err != nil {
return nil, err
}
if err := ext.PipeE(yaml.SetField("setter", y)); err != nil {
return nil, err
}
return object, nil
}
// SetterDefinition may be used to update a files OpenAPI definitions with a new substitution.
type SubstitutionDefinition struct {
// Name is the name of the substitution to create or update
Name string `yaml:"name"`
// Pattern is the substitution pattern into which setter values are substituted
Pattern string `yaml:"pattern"`
// Values are setters which are substituted into pattern to produce a field value
Values []Value `yaml:"values"`
}
type Value struct {
// Marker is the string marker in pattern that is replace by the referenced setter.
Marker string `yaml:"marker"`
// Ref is a reference to a setter to pull the replacement value from.
Ref string `yaml:"ref"`
}
func (sd SubstitutionDefinition) AddToFile(path string) error {
return yaml.UpdateFile(sd, path)
}
func (sd SubstitutionDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) {
// create the substitution extension value by marshalling the SubstitutionDefinition itself
b, err := yaml.Marshal(sd)
if err != nil {
return nil, err
}
sub, err := yaml.Parse(string(b))
if err != nil {
return nil, err
}
// lookup or create the definition for the substitution
defKey := fieldmeta.SubstitutionDefinitionPrefix + sd.Name
def, err := object.Pipe(yaml.LookupCreate(
yaml.MappingNode, openapi.SupplementaryOpenAPIFieldName, "definitions", defKey, "x-k8s-cli"))
if err != nil {
return nil, err
}
// set the substitution on the definition
if err := def.PipeE(yaml.SetField("substitution", sub)); err != nil {
return nil, err
}
return object, nil
}

View File

@@ -1,414 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestAdd_Filter(t *testing.T) {
var tests = []struct {
name string
add Add
input string
expected string
err string
}{
{
name: "add-replicas",
add: Add{
FieldValue: "3",
Ref: "#/definitions/io.k8s.cli.setters.replicas",
},
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3 # {"$openapi":"replicas"}
`,
},
{
name: "add-replicas-annotations",
add: Add{
FieldValue: "3",
Ref: "#/definitions/io.k8s.cli.setters.replicas",
},
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
something: 3
spec:
replicas: 3
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
something: 3 # {"$openapi":"replicas"}
spec:
replicas: 3 # {"$openapi":"replicas"}
`,
},
{
name: "add-replicas-name",
add: Add{
FieldValue: "3",
FieldName: "replicas",
Ref: "#/definitions/io.k8s.cli.setters.replicas",
},
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
something: 3
spec:
replicas: 3
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
something: 3
spec:
replicas: 3 # {"$openapi":"replicas"}
`,
},
{
name: "add-replicas-2x",
add: Add{
FieldValue: "3",
FieldName: "replicas",
Ref: "#/definitions/io.k8s.cli.setters.replicas",
},
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
replicas: 3
spec:
replicas: 3
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
replicas: 3 # {"$openapi":"replicas"}
spec:
replicas: 3 # {"$openapi":"replicas"}
`,
},
{
name: "add-replicas-1x",
add: Add{
FieldValue: "3",
FieldName: "spec.replicas",
Ref: "#/definitions/io.k8s.cli.setters.replicas",
},
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
replicas: 3
spec:
replicas: 3
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
replicas: 3
spec:
replicas: 3 # {"$openapi":"replicas"}
`,
},
{
name: "add-field-inside-sequence",
add: Add{
FieldValue: "/usr/share/nginx",
FieldName: "spec.containers.volumeMounts.mountPath",
Ref: "#/definitions/io.k8s.cli.setters.mountPath",
},
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: nginx
mountPath: /usr/share/nginx
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: nginx
mountPath: /usr/share/nginx # {"$openapi":"mountPath"}
`,
},
{
name: "add-replicas-error",
add: Add{
Ref: "#/definitions/io.k8s.cli.setters.replicas",
},
err: "must specify either fieldName or fieldValue",
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
`,
},
{
name: "ref has . in name of setter",
add: Add{
FieldValue: "3",
Ref: "#/definitions/io.k8s.cli.setters.foo.bar",
},
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3 # {"$openapi":"foo.bar"}
`,
},
}
for i := range tests {
test := tests[i]
t.Run(test.name, func(t *testing.T) {
// parse the input to be modified
r, err := yaml.Parse(test.input)
if !assert.NoError(t, err) {
t.FailNow()
}
// invoke add
result, err := test.add.Filter(r)
if test.err != "" {
if !assert.Equal(t, test.err, err.Error()) {
t.FailNow()
}
return
}
if !assert.NoError(t, err) {
t.FailNow()
}
// compare the actual and expected output
actual, err := result.String()
if !assert.NoError(t, err) {
t.FailNow()
}
actual = strings.TrimSpace(actual)
expected := strings.TrimSpace(test.expected)
if !assert.Equal(t, expected, actual) {
t.FailNow()
}
})
}
}
var resourcefile = `apiVersion: resource.dev/v1alpha1
kind: resourcefile
metadata:
name: hello-world-set
upstream:
type: git
git:
commit: 5c1c019b59299a4f6c7edd1ff5ff54d720621bbe
directory: /package-examples/helloworld-set
ref: v0.1.0
packageMetadata:
shortDescription: example package using setters`
func TestAdd_Filter2(t *testing.T) {
path := filepath.Join(os.TempDir(), "resourcefile")
// write initial resourcefile to temp path
err := os.WriteFile(path, []byte(resourcefile), 0644)
if !assert.NoError(t, err) {
t.FailNow()
}
// add a setter definition
sd := SetterDefinition{
Name: "image",
Value: "1",
}
err = sd.AddToFile(path)
if !assert.NoError(t, err) {
t.FailNow()
}
// update setter definition
sd2 := SetterDefinition{
Name: "image",
Value: "2",
}
err = sd2.AddToFile(path)
if !assert.NoError(t, err) {
t.FailNow()
}
b, err := os.ReadFile(path)
if err != nil {
t.FailNow()
}
expected := `apiVersion: resource.dev/v1alpha1
kind: resourcefile
metadata:
name: hello-world-set
upstream:
type: git
git:
commit: 5c1c019b59299a4f6c7edd1ff5ff54d720621bbe
directory: /package-examples/helloworld-set
ref: v0.1.0
packageMetadata:
shortDescription: example package using setters
openAPI:
definitions:
io.k8s.cli.setters.image:
x-k8s-cli:
setter:
name: image
value: "2"
`
assert.Equal(t, expected, string(b))
}
func TestAddUpdateSubstitution(t *testing.T) {
path := filepath.Join(os.TempDir(), "resourcefile")
// write initial resourcefile to temp path
err := os.WriteFile(path, []byte(resourcefile), 0644)
if !assert.NoError(t, err) {
t.FailNow()
}
value1 := Value{
Marker: "IMAGE_NAME",
Ref: "#/definitions/io.k8s.cli.setters.image-name",
}
value2 := Value{
Marker: "IMAGE_TAG",
Ref: "#/definitions/io.k8s.cli.setters.image-tag",
}
values := []Value{value1, value2}
// add a setter definition
subd := SubstitutionDefinition{
Name: "image",
Pattern: "IMAGE_NAME:IMAGE_TAG",
Values: values,
}
err = subd.AddToFile(path)
if !assert.NoError(t, err) {
t.FailNow()
}
// update setter definition
subd2 := SubstitutionDefinition{
Name: "image",
Pattern: "IMAGE_NAME:IMAGE_TAG2",
}
err = subd2.AddToFile(path)
if !assert.NoError(t, err) {
t.FailNow()
}
b, err := os.ReadFile(path)
if err != nil {
t.FailNow()
}
expected := `apiVersion: resource.dev/v1alpha1
kind: resourcefile
metadata:
name: hello-world-set
upstream:
type: git
git:
commit: 5c1c019b59299a4f6c7edd1ff5ff54d720621bbe
directory: /package-examples/helloworld-set
ref: v0.1.0
packageMetadata:
shortDescription: example package using setters
openAPI:
definitions:
io.k8s.cli.substitutions.image:
x-k8s-cli:
substitution:
name: image
pattern: IMAGE_NAME:IMAGE_TAG2
values: []
`
assert.Equal(t, expected, string(b))
}

View File

@@ -1,171 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"strings"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// Delete delete openAPI definition references from resource fields.
type Delete struct {
// Name is the name of the openAPI definition to delete.
Name string
// DefinitionPrefix is the prefix of the OpenAPI definition type
DefinitionPrefix string
SettersSchema *spec.Schema
}
// Filter implements yaml.Filter
func (d *Delete) Filter(object *yaml.RNode) (*yaml.RNode, error) {
return object, accept(d, object, d.SettersSchema)
}
func (d *Delete) visitSequence(_ *yaml.RNode, _ string, _ *openapi.ResourceSchema) error {
// no-op
return nil
}
func (d *Delete) visitMapping(object *yaml.RNode, _ string, _ *openapi.ResourceSchema) error {
fieldRNodes, err := object.FieldRNodes()
if err != nil {
return err
}
// for each of the field node key visit it as scalar to delete the array setter comment
for _, fieldRNode := range fieldRNodes {
err := d.visitScalar(fieldRNode, "", nil, nil)
if err != nil {
return err
}
}
return nil
}
// visitScalar implements visitor
// visitScalar will remove the reference on each scalar field whose name matches.
func (d *Delete) visitScalar(object *yaml.RNode, _ string, _, _ *openapi.ResourceSchema) error {
// read the field metadata
fm := fieldmeta.FieldMeta{SettersSchema: d.SettersSchema}
if err := fm.Read(object); err != nil {
return err
}
// Delete the reference iff the ref string matches with DefinitionPrefix
if strings.HasSuffix(fm.Schema.Ref.String(), d.DefinitionPrefix+d.Name) {
// remove the ref on the metadata
fm.Schema.Ref = spec.Ref{}
// write the field metadata
if err := fm.Write(object); err != nil {
return err
}
}
return nil
}
// DeleterDefinition may be used to update a files OpenAPI definitions with a new setter.
type DeleterDefinition struct {
// Name is the name of the openAPI definition to delete.
Name string `yaml:"name"`
// DefinitionPrefix is the prefix of the OpenAPI definition type
DefinitionPrefix string `yaml:"definitionPrefix"`
}
func (dd DeleterDefinition) DeleteFromFile(path string) error {
return yaml.UpdateFile(dd, path)
}
// SubstReferringDefinition check if the definition used in substitution and return the substitution name if true
func SubstReferringDefinition(definitions *yaml.RNode, key string) string {
fieldNames, err := definitions.Fields()
if err != nil {
return ""
}
for _, fieldName := range fieldNames {
// the definition key -- contains the substitution name
subkey := definitions.Field(fieldName).Key.YNode().Value
if strings.HasPrefix(subkey, fieldmeta.SubstitutionDefinitionPrefix) {
substNode, err := definitions.Field(fieldName).Value.Pipe(yaml.Lookup(K8sCliExtensionKey, "substitution"))
if err != nil {
continue
}
b, err := substNode.MarshalJSON()
if err != nil {
continue
}
subst := SubstitutionDefinition{}
if err := yaml.Unmarshal(b, &subst); err != nil {
continue
}
// Check the ref in value to see if it contains the setter key
for _, v := range subst.Values {
if strings.HasSuffix(v.Ref, key) {
return subst.Name
}
}
}
}
return ""
}
func (dd DeleterDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) {
key := dd.DefinitionPrefix + dd.Name
var defType string
switch dd.DefinitionPrefix {
case fieldmeta.SubstitutionDefinitionPrefix:
defType = "substitution"
case fieldmeta.SetterDefinitionPrefix:
defType = "setter"
default:
return nil, errors.Errorf("the input delete definitionPrefix does't match any of openAPI definitions, "+
"allowed values [%s, %s]", fieldmeta.SetterDefinitionPrefix, fieldmeta.SubstitutionDefinitionPrefix)
}
definitions, err := object.Pipe(yaml.Lookup(openapi.SupplementaryOpenAPIFieldName, "definitions"))
if err != nil {
return nil, err
}
// return error if the setter to be deleted doesn't exist
if definitions == nil || definitions.Field(key) == nil {
return nil, errors.Errorf("%s %q does not exist", defType, dd.Name)
}
subst := SubstReferringDefinition(definitions, key)
if subst != "" {
return nil, errors.Errorf("%s %q is used in substitution %q, please delete the parent substitution first", defType, dd.Name, subst)
}
_, err = definitions.Pipe(yaml.FieldClearer{Name: key})
if err != nil {
return nil, err
}
// remove definitions if it's empty
_, err = object.Pipe(yaml.Lookup(openapi.SupplementaryOpenAPIFieldName), yaml.FieldClearer{Name: "definitions", IfEmpty: true})
if err != nil {
return nil, err
}
// remove openApi if it's empty
_, err = object.Pipe(yaml.FieldClearer{Name: openapi.SupplementaryOpenAPIFieldName, IfEmpty: true})
if err != nil {
return nil, err
}
return object, nil
}

View File

@@ -1,87 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
)
var resourcefile2 = `apiVersion: resource.dev/v1alpha1
kind: resourcefile
metadata:
name: hello-world-set
upstream:
type: git
git:
commit: 5c1c019b59299a4f6c7edd1ff5ff54d720621bbe
directory: /package-examples/helloworld-set
ref: v0.1.0
packageMetadata:
shortDescription: example package using setters
openAPI:
definitions:
io.k8s.cli.setters.image:
x-k8s-cli:
setter:
name: image
value: "2"
io.k8s.cli.setters.tag:
x-k8s-cli:
setter:
name: tag
value: "sometag"
`
func TestDelete_Filter2(t *testing.T) {
path := filepath.Join(os.TempDir(), "resourcefile2")
// write initial resourcefile to temp path
err := os.WriteFile(path, []byte(resourcefile2), 0644)
if !assert.NoError(t, err) {
t.FailNow()
}
// add a deleter definition
dd := DeleterDefinition{
Name: "image",
DefinitionPrefix: fieldmeta.SetterDefinitionPrefix,
}
err = dd.DeleteFromFile(path)
if !assert.NoError(t, err) {
t.FailNow()
}
b, err := os.ReadFile(path)
if err != nil {
t.FailNow()
}
expected := `apiVersion: resource.dev/v1alpha1
kind: resourcefile
metadata:
name: hello-world-set
upstream:
type: git
git:
commit: 5c1c019b59299a4f6c7edd1ff5ff54d720621bbe
directory: /package-examples/helloworld-set
ref: v0.1.0
packageMetadata:
shortDescription: example package using setters
openAPI:
definitions:
io.k8s.cli.setters.tag:
x-k8s-cli:
setter:
name: tag
value: "sometag"
`
assert.Equal(t, expected, string(b))
}

View File

@@ -1,163 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
//
// Package setters2 contains libraries for setting resource field values from OpenAPI setter
// extensions.
//
// Setters
//
// Setters are used to programmatically set configuration field values -- e.g. through a cli or ui.
//
// Setters are defined through OpenAPI definitions using the x-k8s-cli extension.
// Note: additional OpenAPI definitions may be registered through openapi.AddSchema([]byte)
//
// Example OpenAPI schema containing a setter:
//
// {
// "definitions": {
// "io.k8s.cli.setters.replicas": {
// "x-k8s-cli": {
// "setter": {
// "name": "replicas",
// "value": "4"
// }
// }
// }
// }
// }
//
// Setter fields:
//
// x-k8s-cli.setter.name: name of the setter
// x-k8s-cli.setter.value: value of the setter that should be applied to fields
//
// The setter definition key must be of the form "io.k8s.cli.setters.NAME", where NAME matches the
// value of "x-k8s-cli.setter.name".
//
// When Set.Filter is called, the named setter will have its value applied to all resource
// fields referencing it.
//
// Fields may reference setters through a yaml comment containing the serialized JSON OpenAPI.
//
// Example Deployment resource with a "spec.replicas" field set by the "replicas" setter:
//
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: nginx-deployment
// spec:
// replicas: 4 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
//
// If the OpenAPI io.k8s.cli.setters.replicas x-k8s-cli.setter.value was changed from "4" to "5",
// then calling Set{Name: "replicas"}.Filter(deployment) would update the Deployment spec.replicas
// value from 4 to 5.
//
// Updated OpenAPI:
//
// {
// "definitions": {
// "io.k8s.cli.setters.replicas": {
// "x-k8s-cli": {
// "setter": {
// "name": "replicas",
// "value": "5"
// }
// }
// }
// }
// }
//
// Updated Deployment Configuration:
//
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: nginx-deployment
// spec:
// replicas: 5 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
//
// Substitutions
//
// Substitutions are used to programmatically set configuration field values using multiple
// setters which are substituted into a pattern string.
//
// Substitutions may be used when a field value does not cleanly map to a single setter, but
// instead matches some string pattern where setters may be substituted in.
//
// Fields may reference substitutions the same way they do setters, however substitutions
// reference setters from which they are derived.
//
// Example OpenAPI schema containing a substitution derived from 2 setters:
//
// {
// "definitions": {
// "io.k8s.cli.setters.image-name": {
// "x-k8s-cli": {
// "setter": {
// "name": "image-name",
// "value": "nginx"
// }
// }
// },
// "io.k8s.cli.setters.image-tag": {
// "x-k8s-cli": {
// "setter": {
// "name": "image-tag",
// "value": "1.8.1"
// }
// }
// },
// "io.k8s.cli.substitutions.image-name-tag": {
// "x-k8s-cli": {
// "substitution": {
// "name": "image-name-tag",
// "pattern": "IMAGE_NAME:IMAGE_TAG",
// "values": [
// {"marker": "IMAGE_NAME", "ref": "#/definitions/io.k8s.cli.setters.image-name"}
// {"marker": "IMAGE_TAG", "ref": "#/definitions/io.k8s.cli.setters.image-tag"}
// ]
// }
// }
// }
// }
// }
//
// Substitution Fields.
//
// x-k8s-cli.substitution.name: name of the substitution
// x-k8s-cli.substitution.pattern: string pattern to substitute markers into
// x-k8s-cli.substitution.values.marker: the marker substring within pattern to replace
// x-k8s-cli.substitution.values.ref: the setter ref containing the value to replace the marker with
//
// The substitution is composed of a "pattern" containing markers, and a list of setter "values"
// which are substituted into the markers.
//
// Example Deployment with substitution:
//
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: nginx-deployment
// spec:
// template:
// spec:
// containers:
// - name: nginx
// image: nginx:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image-name-tag"}
//
// spec.template.spec.containers[name=nginx].image is set by the "image" substitution any time
// either "image-name" or "image-tag" is set. Whenever any setter referenced by a substitution
// is set, the substitution will be recalculated by substituting its values into its pattern.
//
//
// If the OpenAPI io.k8s.cli.setters.image-name x-k8s-cli.setter.value was changed from "1.8.1"
// to "1.8.2", then calling either Set{Name: "image-name"}.Filter(deployment) or
// Set{Name: "image-tag"}.Filter(deployment) would update the Deployment field
// spec.template.spec.container[name=nginx].image from "nginx:1.8.1" to "nginx:1.8.2".
//
// Adding Field References
//
// References to setters and substitutions may be added to fields using the Add Filter.
// Add will write a JSON OpenAPI string as a comment to any fields matching the specified
// FieldName add FieldValue.
package setters2

View File

@@ -1,82 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"fmt"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// ExampleAdd demonstrates adding a setter reference to fields.
func ExampleAdd_fieldName() {
deployment := `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
something: 3
spec:
replicas: 3
`
object := yaml.MustParse(deployment) // parse the configuration
err := object.PipeE(&Add{
Ref: "#/definitions/io.k8s.cli.setters.replicas",
FieldName: "replicas",
})
if err != nil {
panic(err)
}
// Print the object with the update value
fmt.Println(object.MustString())
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: nginx-deployment
// annotations:
// something: 3
// spec:
// replicas: 3 # {"$openapi":"replicas"}
}
// ExampleAdd demonstrates adding a setter reference to fields.
func ExampleAdd_fieldValue() {
deployment := `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
something: 3
spec:
replicas: 3
`
object := yaml.MustParse(deployment) // parse the configuration
err := object.PipeE(&Add{
Ref: "#/definitions/io.k8s.cli.setters.replicas",
FieldValue: "3",
})
if err != nil {
panic(err)
}
// Print the object with the update value
fmt.Println(object.MustString())
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: nginx-deployment
// annotations:
// something: 3 # {"$openapi":"replicas"}
// spec:
// replicas: 3 # {"$openapi":"replicas"}
}

View File

@@ -1,195 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"sort"
"strings"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// List lists the setters specified in the OpenAPI
// excludes the subpackages which contain file with
// name OpenAPIFileName in them
type List struct {
Name string
OpenAPIFileName string
Setters []SetterDefinition
Substitutions []SubstitutionDefinition
SettersSchema *spec.Schema
}
// ListSetters initializes l.Setters with the setters from the OpenAPI definitions in the file
func (l *List) ListSetters(openAPIPath, resourcePath string) error {
y, err := yaml.ReadFile(openAPIPath)
if err != nil {
return err
}
return l.listSetters(y, resourcePath)
}
// ListSubst initializes l.Substitutions with the substitutions from the OpenAPI definitions in the file
func (l *List) ListSubst(openAPIPath string) error {
y, err := yaml.ReadFile(openAPIPath)
if err != nil {
return err
}
return l.listSubst(y)
}
func (l *List) listSetters(object *yaml.RNode, resourcePath string) error {
// read the OpenAPI definitions
def, err := object.Pipe(yaml.LookupCreate(yaml.MappingNode, "openAPI", "definitions"))
if err != nil {
return err
}
if yaml.IsMissingOrNull(def) {
return nil
}
// iterate over definitions -- find those that are setters
err = def.VisitFields(func(node *yaml.MapNode) error {
setter := SetterDefinition{}
// the definition key -- contains the setter name
key := node.Key.YNode().Value
if !strings.HasPrefix(key, fieldmeta.SetterDefinitionPrefix) {
// not a setter -- doesn't have the right prefix
return nil
}
setterNode, err := node.Value.Pipe(yaml.Lookup(K8sCliExtensionKey, "setter"))
if err != nil {
return err
}
if yaml.IsMissingOrNull(setterNode) {
// has the setter prefix, but missing the setter extension
return errors.Errorf("missing x-k8s-cli.setter for %s", key)
}
// unmarshal the yaml for the setter extension into the definition struct
b, err := setterNode.String()
if err != nil {
return err
}
if err := yaml.Unmarshal([]byte(b), &setter); err != nil {
return err
}
if l.Name != "" && l.Name != setter.Name {
// not the setter that was requested by list
return nil
}
// the description is not part of the extension, and should be pulled out
// separately from the extension values.
description := node.Value.Field("description")
if description != nil {
setter.Description = description.Value.YNode().Value
}
// count the number of fields set by this setter
setter.Count, err = l.count(resourcePath, setter.Name)
if err != nil {
return err
}
l.Setters = append(l.Setters, setter)
return nil
})
if err != nil {
return err
}
// sort the setters by their name
sort.Slice(l.Setters, func(i, j int) bool {
return l.Setters[i].Name < l.Setters[j].Name
})
return nil
}
func (l *List) listSubst(object *yaml.RNode) error {
// read the OpenAPI definitions
def, err := object.Pipe(yaml.LookupCreate(yaml.MappingNode, "openAPI", "definitions"))
if err != nil {
return err
}
if yaml.IsMissingOrNull(def) {
return nil
}
// iterate over definitions -- find those that are substitutions
err = def.VisitFields(func(node *yaml.MapNode) error {
subst := SubstitutionDefinition{}
// the definition key -- contains the substitution name
key := node.Key.YNode().Value
if !strings.HasPrefix(key, fieldmeta.SubstitutionDefinitionPrefix) {
// not a substitution -- doesn't have the right prefix
return nil
}
substNode, err := node.Value.Pipe(yaml.Lookup(K8sCliExtensionKey, "substitution"))
if err != nil {
return err
}
if yaml.IsMissingOrNull(substNode) {
// has the substitution prefix, but missing the setter extension
return errors.Errorf("missing x-k8s-cli.substitution for %s", key)
}
// unmarshal the yaml for the substitution extension into the definition struct
b, err := substNode.String()
if err != nil {
return err
}
if err := yaml.Unmarshal([]byte(b), &subst); err != nil {
return err
}
if l.Name != "" && l.Name != subst.Name {
// not the substitution that was requested by list
return nil
}
l.Substitutions = append(l.Substitutions, subst)
return nil
})
if err != nil {
return err
}
// sort the substitutions by their name
sort.Slice(l.Substitutions, func(i, j int) bool {
return l.Substitutions[i].Name < l.Substitutions[j].Name
})
return nil
}
// count returns the number of fields set by the setter with name
// this excludes all the subpackages with openAPI file in them
// set filter is leveraged for this but the resources are not written
// back to files as only LocalPackageReader is invoked and not writer
func (l *List) count(path, name string) (int, error) {
s := &Set{Name: name, SettersSchema: l.SettersSchema}
err := kio.Pipeline{
Inputs: []kio.Reader{&kio.LocalPackageReader{PackagePath: path, PackageFileName: l.OpenAPIFileName}},
Filters: []kio.Filter{kio.FilterAll(s)},
}.Execute()
return s.Count, err
}

View File

@@ -1,287 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/openapi"
)
func TestList(t *testing.T) {
var tests = []struct {
name string
setter string
openapi string
input string
expected []SetterDefinition
}{
{
name: "list-replicas",
openapi: `
openAPI:
definitions:
io.k8s.cli.setters.replicas:
x-k8s-cli:
setter:
name: replicas
value: "3"
setBy: me
description: "hello world"
`,
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
`,
expected: []SetterDefinition{
{Name: "replicas", Value: "3", SetBy: "me", Description: "hello world", Count: 1},
},
},
{
name: "list-multiple",
openapi: `
openAPI:
definitions:
io.k8s.cli.setters.replicas:
description: "hello world 1"
x-k8s-cli:
setter:
name: replicas
value: "3"
setBy: me1
io.k8s.cli.setters.image:
description: "hello world 2"
x-k8s-cli:
setter:
name: image
value: "nginx"
setBy: me2
io.k8s.cli.setters.tag:
description: "hello world 3"
x-k8s-cli:
setter:
name: tag
value: "1.7.9"
setBy: me3
io.k8s.cli.substitutions.image:
x-k8s-cli:
substitution:
name: image
pattern: IMAGE:TAG
values:
- marker: IMAGE
ref: '#/definitions/io.k8s.cli.setters.image'
- marker: TAG
ref: '#/definitions/io.k8s.cli.setters.tag'
`,
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
template:
spec:
containers:
- name: nginx
image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"}
- name: nginx2
image: nginx # {"$ref": "#/definitions/io.k8s.cli.setters.image"}
`,
expected: []SetterDefinition{
{Name: "image", Value: "nginx", SetBy: "me2", Description: "hello world 2", Count: 2},
{Name: "replicas", Value: "3", SetBy: "me1", Description: "hello world 1", Count: 1},
{Name: "tag", Value: "1.7.9", SetBy: "me3", Description: "hello world 3", Count: 1},
},
},
{
name: "list-multiple-resources",
openapi: `
openAPI:
definitions:
io.k8s.cli.setters.replicas:
description: "hello world 1"
x-k8s-cli:
setter:
name: replicas
value: "3"
setBy: me1
io.k8s.cli.setters.image:
description: "hello world 2"
x-k8s-cli:
setter:
name: image
value: "nginx"
setBy: me2
io.k8s.cli.setters.tag:
description: "hello world 3"
x-k8s-cli:
setter:
name: tag
value: "1.7.9"
setBy: me3
io.k8s.cli.substitutions.image:
x-k8s-cli:
substitution:
name: image
pattern: IMAGE:TAG
values:
- marker: IMAGE
ref: '#/definitions/io.k8s.cli.setters.image'
- marker: TAG
ref: '#/definitions/io.k8s.cli.setters.tag'
`,
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment-1
spec:
replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
template:
spec:
containers:
- name: nginx
image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"}
- name: nginx2
image: nginx # {"$ref": "#/definitions/io.k8s.cli.setters.image"}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment-2
spec:
replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
template:
spec:
containers:
- name: nginx
image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"}
- name: nginx2
image: nginx
`,
expected: []SetterDefinition{
{Name: "image", Value: "nginx", SetBy: "me2", Description: "hello world 2", Count: 3},
{Name: "replicas", Value: "3", SetBy: "me1", Description: "hello world 1", Count: 2},
{Name: "tag", Value: "1.7.9", SetBy: "me3", Description: "hello world 3", Count: 2},
},
},
{
name: "list-name",
openapi: `
openAPI:
definitions:
io.k8s.cli.setters.replicas:
description: "hello world 1"
x-k8s-cli:
setter:
name: replicas
value: "3"
setBy: me1
io.k8s.cli.setters.image:
description: "hello world 2"
x-k8s-cli:
setter:
name: image
value: "nginx"
setBy: me2
io.k8s.cli.setters.tag:
description: "hello world 3"
x-k8s-cli:
setter:
name: tag
value: "1.7.9"
setBy: me3
io.k8s.cli.substitutions.image:
x-k8s-cli:
substitution:
name: image
pattern: IMAGE:TAG
values:
- marker: IMAGE
ref: '#/definitions/io.k8s.cli.setters.image'
- marker: TAG
ref: '#/definitions/io.k8s.cli.setters.tag'
`,
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment-1
spec:
replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
template:
spec:
containers:
- name: nginx
image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"}
- name: nginx2
image: nginx # {"$ref": "#/definitions/io.k8s.cli.setters.image"}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment-2
spec:
replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
template:
spec:
containers:
- name: nginx
image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"}
- name: nginx2
image: nginx
`,
setter: "image",
expected: []SetterDefinition{
{Name: "image", Value: "nginx", SetBy: "me2", Description: "hello world 2", Count: 3},
},
},
}
for i := range tests {
test := tests[i]
t.Run(test.name, func(t *testing.T) {
// reset the openAPI afterward
defer openapi.ResetOpenAPI()
f, err := os.CreateTemp("", "k8s-cli-")
if !assert.NoError(t, err) {
t.FailNow()
}
defer os.Remove(f.Name())
err = os.WriteFile(f.Name(), []byte(test.openapi), 0600)
if !assert.NoError(t, err) {
t.FailNow()
}
r, err := os.CreateTemp("", "k8s-cli-*.yaml")
if !assert.NoError(t, err) {
t.FailNow()
}
defer os.Remove(r.Name())
err = os.WriteFile(r.Name(), []byte(test.input), 0600)
if !assert.NoError(t, err) {
t.FailNow()
}
// invoke the setter
instance := &List{Name: test.setter, SettersSchema: SettersSchema(t, test.openapi)}
err = instance.ListSetters(f.Name(), r.Name())
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Equal(t, test.expected, instance.Setters) {
t.FailNow()
}
})
}
}

View File

@@ -1,540 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"fmt"
"strings"
"text/template"
"k8s.io/kube-openapi/pkg/validation/spec"
"k8s.io/kube-openapi/pkg/validation/strfmt"
"k8s.io/kube-openapi/pkg/validation/validate"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
goyaml "sigs.k8s.io/kustomize/kyaml/internal/forked/github.com/go-yaml/yaml"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/sets"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// Set sets resource field values from an OpenAPI setter
type Set struct {
// Name is the name of the setter to set on the object. i.e. matches the x-k8s-cli.setter.name
// of the setter that should have its value applied to fields which reference it.
Name string
// Count is the number of fields that were updated by calling Filter
Count int
// SetAll if set to true will set all setters regardless of name
SetAll bool
SettersSchema *spec.Schema
}
// Filter implements Set as a yaml.Filter
func (s *Set) Filter(object *yaml.RNode) (*yaml.RNode, error) {
return object, accept(s, object, s.SettersSchema)
}
// isMatch returns true if the setter with name should have the field
// value set
func (s *Set) isMatch(name string) bool {
return s.SetAll || s.Name == name
}
func (s *Set) visitMapping(_ *yaml.RNode, p string, _ *openapi.ResourceSchema) error {
return nil
}
// visitSequence will perform setters for sequences
func (s *Set) visitSequence(object *yaml.RNode, p string, schema *openapi.ResourceSchema) error {
ext, err := getExtFromComment(schema)
if err != nil {
return err
}
if ext == nil || ext.Setter == nil || !s.isMatch(ext.Setter.Name) ||
len(ext.Setter.ListValues) == 0 {
// setter was not invoked for this sequence
return nil
}
s.Count++
// set the values on the sequences
var elements []*yaml.Node
if len(ext.Setter.ListValues) > 0 {
if err := validateAgainstSchema(ext, schema.Schema); err != nil {
return err
}
}
for i := range ext.Setter.ListValues {
v := ext.Setter.ListValues[i]
n := yaml.NewScalarRNode(v).YNode()
n.Style = yaml.DoubleQuotedStyle
elements = append(elements, n)
}
object.YNode().Content = elements
object.YNode().Style = yaml.FoldedStyle
return nil
}
// visitScalar
func (s *Set) visitScalar(object *yaml.RNode, p string, oa, settersSchema *openapi.ResourceSchema) error {
// get the openAPI for this field describing how to apply the setter
ext, err := getExtFromComment(settersSchema)
if err != nil {
return err
}
if ext == nil {
return nil
}
var k8sSchema *spec.Schema
if oa != nil {
k8sSchema = oa.Schema
}
// perform a direct set of the field if it matches
ok, err := s.set(object, ext, k8sSchema, settersSchema.Schema)
if err != nil {
return err
}
if ok {
s.Count++
return nil
}
// perform a substitution of the field if it matches
sub, err := s.substitute(object, ext)
if err != nil {
return err
}
if sub {
s.Count++
}
return nil
}
// substitute updates the value of field from ext if ext contains a substitution that
// depends on a setter whose name matches s.Name.
func (s *Set) substitute(field *yaml.RNode, ext *CliExtension) (bool, error) {
// check partial setters to see if they contain the setter as part of a
// substitution
if ext.Substitution == nil {
return false, nil
}
// track the visited nodes to detect cycles in nested substitutions
visited := sets.String{}
// nameMatch indicates if the input substitution depends on the specified setter,
// the substitution in ext is parsed recursively and if the setter in Set is hit while
// parsing, it indicates the match
nameMatch := false
res, err := s.substituteUtil(ext, visited, &nameMatch)
if err != nil {
return false, err
}
if !nameMatch {
// doesn't depend on the setter, don't modify its value
return false, nil
}
field.YNode().Value = res
// substitutions are always strings
field.YNode().Tag = yaml.NodeTagString
return true, nil
}
// substituteUtil recursively parses nested substitutions in ext and sets the setter value
// returns error if cyclic substitution is detected or any other unexpected errors
func (s *Set) substituteUtil(ext *CliExtension, visited sets.String, nameMatch *bool) (string, error) {
// check if the substitution has already been visited and throw error as cycles
// are not allowed in nested substitutions
if visited.Has(ext.Substitution.Name) {
return "", errors.Errorf(
"cyclic substitution detected with name " + ext.Substitution.Name)
}
visited.Insert(ext.Substitution.Name)
pattern := ext.Substitution.Pattern
// substitute each setter into the pattern to get the new value
// if substitution references to another substitution, recursively
// process the nested substitutions to replace the pattern with setter values
for _, v := range ext.Substitution.Values {
if v.Ref == "" {
return "", errors.Errorf(
"missing reference on substitution " + ext.Substitution.Name)
}
ref, err := spec.NewRef(v.Ref)
if err != nil {
return "", errors.Wrap(err)
}
def, err := openapi.Resolve(&ref, s.SettersSchema) // resolve the def to its openAPI def
if err != nil {
return "", errors.Wrap(err)
}
defExt, err := GetExtFromSchema(def) // parse the extension out of the openAPI
if err != nil {
return "", errors.Wrap(err)
}
if defExt.Substitution != nil {
// parse recursively if it reference is substitution
substVal, err := s.substituteUtil(defExt, visited, nameMatch)
if err != nil {
return "", err
}
pattern = strings.ReplaceAll(pattern, v.Marker, substVal)
continue
}
// if code reaches this point, this is a setter, so validate the setter schema
if err := validateAgainstSchema(defExt, def); err != nil {
return "", err
}
if s.isMatch(defExt.Setter.Name) {
// the substitution depends on the specified setter
*nameMatch = true
}
if val, found := defExt.Setter.EnumValues[defExt.Setter.Value]; found {
// the setter has an enum-map. we should replace the marker with the
// enum value looked up from the map rather than the enum key
pattern = strings.ReplaceAll(pattern, v.Marker, val)
} else {
pattern = strings.ReplaceAll(pattern, v.Marker, defExt.Setter.Value)
}
}
return pattern, nil
}
// set applies the value from ext to field if its name matches s.Name
func (s *Set) set(field *yaml.RNode, ext *CliExtension, k8sSch, sch *spec.Schema) (bool, error) {
// check full setter
if ext.Setter == nil || !s.isMatch(ext.Setter.Name) {
return false, nil
}
if err := validateAgainstSchema(ext, sch); err != nil {
return false, err
}
if val, found := ext.Setter.EnumValues[ext.Setter.Value]; found {
// the setter has an enum-map. we should replace the marker with the
// enum value looked up from the map rather than the enum key
field.YNode().Value = val
return true, nil
}
// this has a full setter, set its value
field.YNode().Value = ext.Setter.Value
// format the node so it is quoted if it is a string. If there is
// type information on the setter schema, we use it. Otherwise we
// fall back to the field schema if it exists.
if len(sch.Type) > 0 {
yaml.FormatNonStringStyle(field.YNode(), *sch)
} else if k8sSch != nil {
yaml.FormatNonStringStyle(field.YNode(), *k8sSch)
}
return true, nil
}
// validateAgainstSchema validates the input setter value against user provided
// openAI schema
func validateAgainstSchema(ext *CliExtension, sch *spec.Schema) error {
fixSchemaTypes(sch)
sc := spec.Schema{}
sc.Properties = map[string]spec.Schema{}
sc.Properties[ext.Setter.Name] = *sch
var inputYAML string
if len(ext.Setter.ListValues) > 0 {
// tmplText contains the template we will use to produce a yaml
// document that we can use for validation.
var tmplText string
if sch.Items != nil && sch.Items.Schema != nil &&
shouldQuoteSetterValue(ext.Setter.ListValues, sch.Items.Schema.Type) {
// If string is one of the legal types for the value, we
// output it with quotes in the yaml document to make sure it
// is later parsed as a string.
tmplText = `{{.key}}:{{block "list" .values}}{{"\n"}}{{range .}}{{printf "- %q\n" .}}{{end}}{{end}}`
} else {
// If string is not specifically set as the type, we just
// let the yaml unmarshaller detect the correct type. Thus, we
// do not add quotes around the value.
tmplText = `{{.key}}:{{block "list" .values}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
}
tmpl, err := template.New("validator").Parse(tmplText)
if err != nil {
return err
}
var builder strings.Builder
err = tmpl.Execute(&builder, map[string]interface{}{
"key": ext.Setter.Name,
"values": ext.Setter.ListValues,
})
if err != nil {
return err
}
inputYAML = builder.String()
} else {
var format string
// Only add quotes around the value is string is one of the
// types in the schema.
if shouldQuoteSetterValue([]string{ext.Setter.Value}, sch.Type) {
format = "%s: \"%s\""
} else {
format = "%s: %s"
}
inputYAML = fmt.Sprintf(format, ext.Setter.Name, ext.Setter.Value)
}
input := map[string]interface{}{}
err := goyaml.Unmarshal([]byte(inputYAML), &input)
if err != nil {
return err
}
err = validate.AgainstSchema(&sc, input, strfmt.Default)
if err != nil {
return errors.Errorf("The input value doesn't validate against provided OpenAPI schema: %v\n", err.Error())
}
return nil
}
// shouldQuoteSetterValue returns true if string is one of the types in the
// schema, or if the value ends in a ':' (the yaml parser gets confused by
// the ':' at the end unless the value is quoted)
func shouldQuoteSetterValue(a []string, schType spec.StringOrArray) bool {
if schType.Contains("string") {
return true
}
for _, s := range a {
if strings.HasSuffix(s, ":") {
return true
}
}
return false
}
// fixSchemaTypes traverses the schema and checks for some common
// errors for the type field. This currently involves users using
// 'int' instead of 'integer' and 'bool' instead of 'boolean'. Early versions
// of setters didn't validate this, so there are users that have invalid
// types in their packages.
func fixSchemaTypes(sc *spec.Schema) {
for i := range sc.Type {
currentType := sc.Type[i]
if currentType == "int" {
sc.Type[i] = "integer"
}
if currentType == "bool" {
sc.Type[i] = "boolean"
}
}
if items := sc.Items; items != nil {
if items.Schema != nil {
fixSchemaTypes(items.Schema)
}
for i := range items.Schemas {
schema := items.Schemas[i]
fixSchemaTypes(&schema)
}
}
}
// SetOpenAPI updates a setter value
type SetOpenAPI struct {
// Name is the name of the setter to add
Name string `yaml:"name"`
// Value is the current value of the setter
Value string `yaml:"value"`
// ListValue is the current value for a list of items
ListValues []string `yaml:"listValue"`
Description string `yaml:"description"`
SetBy string `yaml:"setBy"`
IsSet bool `yaml:"isSet"`
}
// UpdateFile updates the OpenAPI definitions in a file with the given setter value.
func (s SetOpenAPI) UpdateFile(path string) error {
return yaml.UpdateFile(s, path)
}
func (s SetOpenAPI) Filter(object *yaml.RNode) (*yaml.RNode, error) {
key := fieldmeta.SetterDefinitionPrefix + s.Name
oa, err := object.Pipe(yaml.Lookup("openAPI", "definitions", key))
if err != nil {
return nil, err
}
if oa == nil {
return nil, errors.Errorf("setter %q is not found", s.Name)
}
def, err := oa.Pipe(yaml.Lookup("x-k8s-cli", "setter"))
if err != nil {
return nil, err
}
if def == nil {
return nil, errors.Errorf("setter %q is not found", s.Name)
}
// record the OpenAPI type for the setter
var t string
if n := oa.Field("type"); n != nil {
t = n.Value.YNode().Value
}
// if the setter contains an enumValues map, then ensure the set value appears
// as a key in the map
if values, err := def.Pipe(
yaml.Lookup("enumValues")); err != nil {
// error looking up the enumValues
return nil, err
} else if values != nil {
// contains enumValues map -- validate the set value against the map entries
// get the enumValues keys
fields, err := values.Fields()
if err != nil {
return nil, err
}
// search for the user provided value in the set of allowed values
var match bool
for i := range fields {
if fields[i] == s.Value {
// found a match, we are good
match = true
break
}
}
if !match {
// no match found -- provide an informative error to the user
return nil, errors.Errorf("%s does not match the possible values for %s: [%s]",
s.Value, s.Name, strings.Join(fields, ","))
}
}
v := yaml.NewScalarRNode(s.Value)
// values are always represented as strings the OpenAPI
// since the are unmarshalled into strings. Use double quote style to
// ensure this consistently.
v.YNode().Tag = yaml.NodeTagString
v.YNode().Style = yaml.DoubleQuotedStyle
if t != "array" {
// set a scalar value
if err := def.PipeE(&yaml.FieldSetter{Name: "value", Value: v}); err != nil {
return nil, err
}
} else {
// set a list value
if err := def.PipeE(&yaml.FieldClearer{Name: "value"}); err != nil {
return nil, err
}
// create the list values
var elements []*yaml.Node
n := yaml.NewScalarRNode(s.Value).YNode()
n.Tag = yaml.NodeTagString
n.Style = yaml.DoubleQuotedStyle
elements = append(elements, n)
for i := range s.ListValues {
v := s.ListValues[i]
n := yaml.NewScalarRNode(v).YNode()
n.Style = yaml.DoubleQuotedStyle
elements = append(elements, n)
}
l := yaml.NewRNode(&yaml.Node{
Kind: yaml.SequenceNode,
Content: elements,
})
def.YNode().Style = yaml.FoldedStyle
if err := def.PipeE(&yaml.FieldSetter{Name: "listValues", Value: l}); err != nil {
return nil, err
}
}
if err := def.PipeE(&yaml.FieldSetter{Name: "setBy", StringValue: s.SetBy}); err != nil {
return nil, err
}
if s.IsSet {
n := &yaml.Node{
Kind: yaml.ScalarNode,
Value: "true",
Tag: yaml.NodeTagBool,
}
if err := def.PipeE(&yaml.FieldSetter{Name: "isSet", Value: yaml.NewRNode(n)}); err != nil {
return nil, err
}
}
if s.Description != "" {
d, err := object.Pipe(yaml.LookupCreate(
yaml.MappingNode, "openAPI", "definitions", key))
if err != nil {
return nil, err
}
if err := d.PipeE(&yaml.FieldSetter{Name: "description", StringValue: s.Description}); err != nil {
return nil, err
}
}
return object, nil
}
// SetAll applies the set filter for all yaml nodes and only returns the nodes whose
// corresponding file has at least one node with input setter
func SetAll(s *Set) kio.Filter {
return kio.FilterFunc(func(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
filesToUpdate := sets.String{}
// for each node record the set fields count before and after filter is applied and
// store the corresponding file paths if there is an increment in setters count
for i := range nodes {
preCount := s.Count
_, err := s.Filter(nodes[i])
if err != nil {
return nil, errors.Wrap(err)
}
if s.Count > preCount {
path, _, err := kioutil.GetFileAnnotations(nodes[i])
if err != nil {
return nil, errors.Wrap(err)
}
filesToUpdate.Insert(path)
}
}
var nodesInUpdatedFiles []*yaml.RNode
// return only the nodes whose corresponding file has at least one node with input setter
for i := range nodes {
path, _, err := kioutil.GetFileAnnotations(nodes[i])
if err != nil {
return nil, errors.Wrap(err)
}
if filesToUpdate.Has(path) {
nodesInUpdatedFiles = append(nodesInUpdatedFiles, nodes[i])
}
}
return nodesInUpdatedFiles, nil
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,118 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package settersutil
import (
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
"sigs.k8s.io/kustomize/kyaml/openapi"
)
var openAPIFile = `
openAPI:
definitions:
io.k8s.cli.setters.image:
x-k8s-cli:
setter:
name: image
value: "2"
io.k8s.cli.setters.tag:
x-k8s-cli:
setter:
name: tag
value: "sometag"
`
var resourceFile = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
image: 3 # {"$openapi":"image"}
spec:
image: 3 # {"$openapi":"image"}
`
func TestDeleterCreator_Delete(t *testing.T) {
openapi.ResetOpenAPI()
defer openapi.ResetOpenAPI()
openAPI, err := os.CreateTemp("", "openAPI.yaml")
if !assert.NoError(t, err) {
t.FailNow()
}
defer os.Remove(openAPI.Name())
// write openapi to temp dir
err = os.WriteFile(openAPI.Name(), []byte(openAPIFile), 0644)
if !assert.NoError(t, err) {
t.FailNow()
}
// write resource file to temp dir
resource, err := os.CreateTemp("", "k8s-cli-*.yaml")
if !assert.NoError(t, err) {
t.FailNow()
}
defer os.Remove(resource.Name())
err = os.WriteFile(resource.Name(), []byte(resourceFile), 0644)
if !assert.NoError(t, err) {
t.FailNow()
}
sc, err := openapi.SchemaFromFile(openAPI.Name())
if !assert.NoError(t, err) {
t.FailNow()
}
// add a delete creator
dc := DeleterCreator{
Name: "image",
DefinitionPrefix: fieldmeta.SetterDefinitionPrefix,
SettersSchema: sc,
}
dc.OpenAPIPath = openAPI.Name()
dc.ResourcesPath = resource.Name()
err = dc.Delete()
if !assert.NoError(t, err) {
t.FailNow()
}
actualOpenAPI, err := os.ReadFile(openAPI.Name())
if err != nil {
t.FailNow()
}
actualResource, err := os.ReadFile(resource.Name())
if err != nil {
t.FailNow()
}
expectedOpenAPI := `
openAPI:
definitions:
io.k8s.cli.setters.tag:
x-k8s-cli:
setter:
name: tag
value: "sometag"
`
expectedResoure := `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
image: 3
spec:
image: 3
`
assert.Equal(t, strings.TrimSpace(expectedOpenAPI), strings.TrimSpace(string(actualOpenAPI)))
assert.Equal(t, strings.TrimSpace(expectedResoure), strings.TrimSpace(string(actualResource)))
}

View File

@@ -1,55 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package settersutil
import (
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/setters2"
)
// DeleterCreator delete a definition in the OpenAPI definitions, and removes references
// to the definition from matching resource fields.
type DeleterCreator struct {
// Name is the name of the setter or substitution to delete
Name string
// DefinitionPrefix is the prefix of the OpenAPI definition type
DefinitionPrefix string
RecurseSubPackages bool
OpenAPIFileName string
// Path to openAPI file
OpenAPIPath string
// Path to resources folder
ResourcesPath string
SettersSchema *spec.Schema
}
func (d DeleterCreator) Delete() error {
dd := setters2.DeleterDefinition{
Name: d.Name,
DefinitionPrefix: d.DefinitionPrefix,
}
if err := dd.DeleteFromFile(d.OpenAPIPath); err != nil {
return err
}
// Update the resources with the deleter reference
inout := &kio.LocalPackageReadWriter{PackagePath: d.ResourcesPath, PackageFileName: d.OpenAPIFileName}
return kio.Pipeline{
Inputs: []kio.Reader{inout},
Filters: []kio.Filter{kio.FilterAll(
&setters2.Delete{
Name: d.Name,
DefinitionPrefix: d.DefinitionPrefix,
SettersSchema: d.SettersSchema,
})},
Outputs: []kio.Writer{inout},
}.Execute()
}

View File

@@ -1,138 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package settersutil
import (
"os"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/setters2"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// FieldSetter sets the value for a field setter.
type FieldSetter struct {
// Name is the name of the setter to set
Name string
// Value is the value to set
Value string
// ListValues contains a list of values to set on a Sequence
ListValues []string
Description string
SetBy string
Count int
OpenAPIPath string
OpenAPIFileName string
ResourcesPath string
RecurseSubPackages bool
IsSet bool
SettersSchema *spec.Schema
}
func (fs *FieldSetter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
fs.Count, _ = fs.Set()
return nil, nil
}
// Set updates the OpenAPI definitions and resources with the new setter value
func (fs *FieldSetter) Set() (int, error) {
// Update the OpenAPI definitions
soa := setters2.SetOpenAPI{
Name: fs.Name,
Value: fs.Value,
ListValues: fs.ListValues,
Description: fs.Description,
SetBy: fs.SetBy,
IsSet: fs.IsSet,
}
// the input field value is updated in the openAPI file and then parsed
// at to get the value and set it to resource files, but if there is error
// after updating openAPI file and while updating resources, the openAPI
// file should be reverted, as set operation failed
stat, err := os.Stat(fs.OpenAPIPath)
if err != nil {
return 0, err
}
curOpenAPI, err := os.ReadFile(fs.OpenAPIPath)
if err != nil {
return 0, err
}
// write the new input value to openAPI file
if err := soa.UpdateFile(fs.OpenAPIPath); err != nil {
return 0, err
}
// Load the updated definitions
sc, err := openapi.SchemaFromFile(fs.OpenAPIPath)
if err != nil {
return 0, err
}
fs.SettersSchema = sc
// Update the resources with the new value
// Set NoDeleteFiles to true as SetAll will return only the nodes of files which should be updated and
// hence, rest of the files should not be deleted
inout := &kio.LocalPackageReadWriter{PackagePath: fs.ResourcesPath, NoDeleteFiles: true, PackageFileName: fs.OpenAPIFileName}
s := &setters2.Set{Name: fs.Name, SettersSchema: sc}
err = kio.Pipeline{
Inputs: []kio.Reader{inout},
Filters: []kio.Filter{setters2.SetAll(s)},
Outputs: []kio.Writer{inout},
}.Execute()
// revert openAPI file if set operation fails
if err != nil {
if writeErr := os.WriteFile(fs.OpenAPIPath, curOpenAPI, stat.Mode().Perm()); writeErr != nil {
return 0, writeErr
}
}
return s.Count, err
}
// SetAllSetterDefinitions reads all the Setter Definitions from the input OpenAPI
// file and sets all values for the resource configs in the provided destination directories.
// If syncOpenAPI is true, the openAPI files in destination directories are also
// updated with the setter values in the input openAPI file
func SetAllSetterDefinitions(openAPIPath string, dirs ...string) error {
sc, err := openapi.SchemaFromFile(openAPIPath)
if err != nil {
return err
}
for _, destDir := range dirs {
rw := &kio.LocalPackageReadWriter{
PackagePath: destDir,
// set output won't include resources from files which
// weren't modified. make sure we don't delete them.
NoDeleteFiles: true,
}
// apply all of the setters to the directory
err := kio.Pipeline{
Inputs: []kio.Reader{rw},
// Set all of the setters
Filters: []kio.Filter{setters2.SetAll(&setters2.Set{SetAll: true, SettersSchema: sc})},
Outputs: []kio.Writer{rw},
}.Execute()
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,124 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package settersutil
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSetAllSetterDefinitions(t *testing.T) {
var tests = []struct {
name string
srcOpenAPIFile string
destFile string
destOpenAPI string
expectedDestFile string
expectedDestOpenAPIFile string
}{
{
name: "set values only to resources and not the openAPI",
srcOpenAPIFile: `openAPI:
definitions:
io.k8s.cli.setters.namespace:
x-k8s-cli:
setter:
name: namespace
value: "project-namespace"
io.k8s.cli.setters.replicas:
x-k8s-cli:
setter:
name: replicas
value: "4"`,
destFile: `apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: some-other-namespace # {"$ref": "#/definitions/io.k8s.cli.setters.namespace"}
spec:
replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas-new"}`,
destOpenAPI: `openAPI:
definitions:
io.k8s.cli.setters.namespace:
x-k8s-cli:
setter:
name: namespace
value: "some-other-namespace"
io.k8s.cli.setters.replicas-new:
x-k8s-cli:
setter:
name: replicas-new
value: "3"`,
expectedDestFile: `apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: project-namespace # {"$ref": "#/definitions/io.k8s.cli.setters.namespace"}
spec:
replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas-new"}
`,
expectedDestOpenAPIFile: `openAPI:
definitions:
io.k8s.cli.setters.namespace:
x-k8s-cli:
setter:
name: namespace
value: "some-other-namespace"
io.k8s.cli.setters.replicas-new:
x-k8s-cli:
setter:
name: replicas-new
value: "3"`,
},
}
for i := range tests {
test := tests[i]
t.Run(test.name, func(t *testing.T) {
srcDir := t.TempDir()
destDir := t.TempDir()
err := os.WriteFile(filepath.Join(srcDir, "Krmfile"), []byte(test.srcOpenAPIFile), 0600)
if !assert.NoError(t, err) {
t.FailNow()
}
err = os.WriteFile(filepath.Join(destDir, "destFile.yaml"), []byte(test.destFile), 0600)
if !assert.NoError(t, err) {
t.FailNow()
}
err = os.WriteFile(filepath.Join(destDir, "Krmfile"), []byte(test.destOpenAPI), 0600)
if !assert.NoError(t, err) {
t.FailNow()
}
err = SetAllSetterDefinitions(filepath.Join(srcDir, "Krmfile"), destDir)
if !assert.NoError(t, err) {
t.FailNow()
}
actualDestFile1, err := os.ReadFile(filepath.Join(destDir, "destFile.yaml"))
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Equal(t, test.expectedDestFile, string(actualDestFile1)) {
t.FailNow()
}
actualDestOpenAPIFile1, err := os.ReadFile(filepath.Join(destDir, "Krmfile"))
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.Equal(t, test.expectedDestOpenAPIFile, string(actualDestOpenAPIFile1)) {
t.FailNow()
}
})
}
}

View File

@@ -1,209 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package settersutil
import (
"fmt"
"strings"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/setters2"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// SetterCreator creates or updates a setter in the OpenAPI definitions, and inserts references
// to the setter from matching resource fields.
type SetterCreator struct {
// Name is the name of the setter to create or update.
Name string
SetBy string
Description string
Type string
Schema string
// FieldName if set will add the OpenAPI reference to fields with this name or path
// FieldName may be the full name of the field, full path to the field, or the path suffix.
// e.g. all of the following would match spec.template.spec.containers.image --
// [image, containers.image, spec.containers.image, template.spec.containers.image,
// spec.template.spec.containers.image]
// Optional. If unspecified match all field names.
FieldName string
// FieldValue if set will add the OpenAPI reference to fields if they have this value.
// Optional. If unspecified match all field values.
FieldValue string
// Required indicates that the setter must be set by package consumer before
// live apply/preview. This field is added to the setter definition to record
// the package publisher's intent to make the setter required to be set.
Required bool
RecurseSubPackages bool
OpenAPIFileName string
// Path to openAPI file
OpenAPIPath string
// Path to resources folder
ResourcesPath string
SettersSchema *spec.Schema
}
func (c *SetterCreator) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
return nil, c.Create()
}
func (c *SetterCreator) Create() error {
err := c.validateSetterInfo()
if err != nil {
return err
}
err = validateSchema(c.Schema)
if err != nil {
return errors.Errorf("invalid schema: %v", err)
}
// Update the resources with the setter reference
inout := &kio.LocalPackageReadWriter{PackagePath: c.ResourcesPath}
a := &setters2.Add{
FieldName: c.FieldName,
FieldValue: c.FieldValue,
Ref: fieldmeta.DefinitionsPrefix + fieldmeta.SetterDefinitionPrefix + c.Name,
Type: c.Type,
SettersSchema: c.SettersSchema,
}
err = kio.Pipeline{
Inputs: []kio.Reader{inout},
Filters: []kio.Filter{kio.FilterAll(a)},
Outputs: []kio.Writer{inout},
}.Execute()
if a.Count == 0 {
fmt.Printf("setter %q doesn't match any field in resource configs, "+
"but creating setter definition\n", c.Name)
}
if err != nil {
return err
}
// Update the OpenAPI definitions to hace the setter
sd := setters2.SetterDefinition{
Name: c.Name, Value: c.FieldValue, SetBy: c.SetBy, Description: c.Description,
Type: c.Type, Schema: c.Schema, Required: c.Required,
}
if err := sd.AddToFile(c.OpenAPIPath); err != nil {
return err
}
// Load the updated definitions
sc, err := openapi.SchemaFromFile(c.OpenAPIPath)
if err != nil {
return err
}
c.SettersSchema = sc
// if the setter is of array type write the derived list values back to
// openAPI definitions
if len(a.ListValues) > 0 {
sd.ListValues = a.ListValues
sd.Value = ""
if err := sd.AddToFile(c.OpenAPIPath); err != nil {
return err
}
}
return nil
}
// The types recognized by by the go openapi validation library:
// https://github.com/go-openapi/validate/blob/master/helpers.go#L35
var validTypeValues = []string{
"object", "array", "string", "integer", "number", "boolean", "file", "null",
}
// validateSchema parses the provided schema and validates it.
func validateSchema(schema string) error {
var sc spec.Schema
err := sc.UnmarshalJSON([]byte(schema))
if err != nil {
return errors.Errorf("unable to parse schema: %v", err)
}
return validateSchemaTypes(&sc)
}
// validateSchemaTypes traverses the schema and checks that only valid types
// are used.
func validateSchemaTypes(sc *spec.Schema) error {
if len(sc.Type) > 1 {
return errors.Errorf("only one type is supported: %s", strings.Join(sc.Type, ", "))
}
if len(sc.Type) == 1 {
t := sc.Type[0]
var match bool
for _, validType := range validTypeValues {
if t == validType {
match = true
}
}
if !match {
return errors.Errorf("type %q is not supported. Must be one of: %s",
t, strings.Join(validTypeValues, ", "))
}
}
if items := sc.Items; items != nil {
if items.Schema != nil {
err := validateSchemaTypes(items.Schema)
if err != nil {
return err
}
}
for i := range items.Schemas {
schema := items.Schemas[i]
err := validateSchemaTypes(&schema)
if err != nil {
return err
}
}
}
return nil
}
func (c SetterCreator) validateSetterInfo() error {
// check if substitution with same name exists and throw error
ref, err := spec.NewRef(fieldmeta.DefinitionsPrefix + fieldmeta.SubstitutionDefinitionPrefix + c.Name)
if err != nil {
return err
}
subst, _ := openapi.Resolve(&ref, c.SettersSchema)
// if substitution already exists with the input setter name, throw error
if subst != nil {
return errors.Errorf("substitution with name %q already exists, "+
"substitution and setter can't have same name", c.Name)
}
// check if setter with same name exists and throw error
ref, err = spec.NewRef(fieldmeta.DefinitionsPrefix + fieldmeta.SetterDefinitionPrefix + c.Name)
if err != nil {
return err
}
setter, _ := openapi.Resolve(&ref, c.SettersSchema)
// if setter already exists with the input setter name, throw error
if setter != nil {
return errors.Errorf("setter with name %q already exists, "+
"if you want to modify it, please delete the existing setter and recreate it", c.Name)
}
return nil
}

View File

@@ -1,465 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package settersutil
import (
"fmt"
"os"
"regexp"
"strings"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/sets"
"sigs.k8s.io/kustomize/kyaml/setters2"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// SubstitutionCreator creates or updates a substitution in the OpenAPI definitions, and
// inserts references to the substitution from matching resource fields.
type SubstitutionCreator struct {
// Name is the name of the substitution to create
Name string
// Pattern is the substitution pattern
Pattern string
// Values are the substitution values for the pattern
Values []setters2.Value
// FieldName if set will add the OpenAPI reference to fields with this name or path
// FieldName may be the full name of the field, full path to the field, or the path suffix.
// e.g. all of the following would match spec.template.spec.containers.image --
// [image, containers.image, spec.containers.image, template.spec.containers.image,
// spec.template.spec.containers.image]
// Optional. If unspecified match all field names.
FieldName string
// FieldValue if set will add the OpenAPI reference to fields if they have this value.
// Optional. If unspecified match all field values.
FieldValue string
// Path to openAPI file
OpenAPIPath string
OpenAPIFileName string
RecurseSubPackages bool
// Path to resources folder
ResourcesPath string
SettersSchema *spec.Schema
}
func (c *SubstitutionCreator) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
return nil, c.Create()
}
func (c SubstitutionCreator) Create() error {
err := c.validateSubstitutionInfo()
if err != nil {
return err
}
values, err := c.markersAndRefs(c.Name, c.Pattern)
if err != nil {
return err
}
c.Values = values
d := setters2.SubstitutionDefinition{
Name: c.Name,
Values: c.Values,
Pattern: c.Pattern,
}
// the input substitution definition is updated in the openAPI file and then parsed
// to check if there are any cycles in nested substitutions, if there are
// any, the openAPI file will be reverted to current state and error is thrown
stat, err := os.Stat(c.OpenAPIPath)
if err != nil {
return err
}
curOpenAPI, err := os.ReadFile(c.OpenAPIPath)
if err != nil {
return err
}
if err := d.AddToFile(c.OpenAPIPath); err != nil {
return err
}
// Load the updated definitions
sc, err := openapi.SchemaFromFile(c.OpenAPIPath)
if err != nil {
return err
}
c.SettersSchema = sc
visited := sets.String{}
ref, err := spec.NewRef(fieldmeta.DefinitionsPrefix + fieldmeta.SubstitutionDefinitionPrefix + c.Name)
if err != nil {
return err
}
schema, err := openapi.Resolve(&ref, c.SettersSchema)
if err != nil {
return err
}
ext, err := setters2.GetExtFromSchema(schema)
if err != nil {
return err
}
err = c.CreateSettersForSubstitution(c.OpenAPIPath)
if err != nil {
return err
}
// Load the updated definitions after setters are created
sc, err = openapi.SchemaFromFile(c.OpenAPIPath)
if err != nil {
return err
}
c.SettersSchema = sc
// revert openAPI file if there are cycles detected in created input substitution
if err := c.checkForCycles(ext, visited); err != nil {
if writeErr := os.WriteFile(c.OpenAPIPath, curOpenAPI, stat.Mode().Perm()); writeErr != nil {
return writeErr
}
return err
}
a := &setters2.Add{
FieldName: c.FieldName,
FieldValue: c.FieldValue,
Ref: fieldmeta.DefinitionsPrefix + fieldmeta.SubstitutionDefinitionPrefix + c.Name,
SettersSchema: c.SettersSchema,
}
// Update the resources with the substitution reference
inout := &kio.LocalPackageReadWriter{PackagePath: c.ResourcesPath, PackageFileName: c.OpenAPIFileName}
err = kio.Pipeline{
Inputs: []kio.Reader{inout},
Filters: []kio.Filter{kio.FilterAll(a)},
Outputs: []kio.Writer{inout},
}.Execute()
if a.Count == 0 {
fmt.Printf("substitution %s doesn't match any field value in resource configs, "+
"but creating substitution definition\n", c.Name)
}
return err
}
// createMarkersAndRefs takes the input pattern, creates setter/substitution markers
// and corresponding openAPI refs
func (c *SubstitutionCreator) markersAndRefs(substName, pattern string) ([]setters2.Value, error) {
var values []setters2.Value
// extract setter name tokens from pattern enclosed in ${}
re := regexp.MustCompile(`\$\{([^}]*)\}`)
markers := re.FindAllString(pattern, -1)
if len(markers) == 0 {
return nil, errors.Errorf("unable to find setter or substitution names in pattern, " +
"setter names must be enclosed in ${}")
}
for _, marker := range markers {
name := strings.TrimSuffix(strings.TrimPrefix(marker, "${"), "}")
if name == substName {
return nil, fmt.Errorf("setters must have different name than the substitution: %s", name)
}
ref, err := spec.NewRef(fieldmeta.DefinitionsPrefix + fieldmeta.SubstitutionDefinitionPrefix + name)
if err != nil {
return nil, err
}
var markerRef string
subst, _ := openapi.Resolve(&ref, c.SettersSchema)
// check if the substitution exists with the marker name or fall back to creating setter
// ref with the name
if subst != nil {
markerRef = fieldmeta.DefinitionsPrefix + fieldmeta.SubstitutionDefinitionPrefix + name
} else {
markerRef = fieldmeta.DefinitionsPrefix + fieldmeta.SetterDefinitionPrefix + name
}
values = append(
values,
setters2.Value{Marker: marker, Ref: markerRef},
)
}
return values, nil
}
// CreateSettersForSubstitution creates the setters for all the references in the substitution
// values if they don't already exist in openAPIPath file.
func (c SubstitutionCreator) CreateSettersForSubstitution(openAPIPath string) error {
y, err := yaml.ReadFile(openAPIPath)
if err != nil {
return err
}
m, err := c.GetValuesForMarkers()
if err != nil {
return err
}
// for each ref in values, check if the setter or substitution already exists, if not create setter
for _, value := range c.Values {
// continue if ref is a substitution, as it has already been checked if it exists
// as part of preRunE
if strings.Contains(value.Ref, fieldmeta.SubstitutionDefinitionPrefix) {
fmt.Printf("found a substitution with name %q\n", value.Marker)
continue
}
setterObj, err := y.Pipe(yaml.Lookup(
// get the setter key from ref. Ex: from #/definitions/io.k8s.cli.setters.image_setter
// extract io.k8s.cli.setters.image_setter
"openAPI", "definitions", strings.TrimPrefix(value.Ref, fieldmeta.DefinitionsPrefix)))
if err != nil {
return err
}
if setterObj == nil {
name := strings.TrimPrefix(value.Ref, fieldmeta.DefinitionsPrefix+fieldmeta.SetterDefinitionPrefix)
value := m[value.Marker]
fmt.Printf("unable to find setter with name %s, creating new setter with value %s\n", name, value)
sd := setters2.SetterDefinition{
// get the setter name from ref. Ex: from #/definitions/io.k8s.cli.setters.image_setter
// extract image_setter
Name: name,
Value: value,
}
err := sd.AddToFile(openAPIPath)
if err != nil {
return err
}
}
}
return nil
}
func (c SubstitutionCreator) checkForCycles(ext *setters2.CliExtension, visited sets.String) error {
// check if the substitution has already been visited and throw error as cycles
// are not allowed in nested substitutions
if visited.Has(ext.Substitution.Name) {
return errors.Errorf(
"cyclic substitution detected with name " + ext.Substitution.Name)
}
visited.Insert(ext.Substitution.Name)
// substitute each setter into the pattern to get the new value
// if substitution references to another substitution, recursively
// process the nested substitutions to replace the pattern with setter values
for _, v := range ext.Substitution.Values {
if v.Ref == "" {
return errors.Errorf(
"missing reference on substitution " + ext.Substitution.Name)
}
ref, err := spec.NewRef(v.Ref)
if err != nil {
return errors.Wrap(err)
}
def, err := openapi.Resolve(&ref, c.SettersSchema) // resolve the def to its openAPI def
if err != nil {
return errors.Wrap(err)
}
defExt, err := setters2.GetExtFromSchema(def) // parse the extension out of the openAPI
if err != nil {
return errors.Wrap(err)
}
if defExt.Substitution != nil {
// parse recursively if it reference is substitution
err := c.checkForCycles(defExt, visited)
if err != nil {
return err
}
}
}
return nil
}
// GetValuesForMarkers parses the pattern and field value to derive values for the
// markers in the pattern string. Returns error if the marker values can't be derived
func (c SubstitutionCreator) GetValuesForMarkers() (map[string]string, error) {
m := make(map[string]string)
indices, err := c.GetStartIndices()
if err != nil {
return nil, err
}
fv := c.FieldValue
pattern := c.Pattern
fvInd := 0
patternInd := 0
// iterate fv, pattern with indices fvInd, patternInd respectively and when patternInd hits the index of a marker,
// freeze patternInd and iterate fvInd and capture string till we find the substring just after current marker
// and before next marker
// Ex: fv = "something/ubuntu:0.1.0", pattern = "something/IMAGE:VERSION", till patternInd reaches 10
// just proceed fvInd and patternInd and check if fv[fvInd]==pattern[patternInd] when patternInd is 10,
// freeze patternInd and move fvInd till it sees substring ':' which derives IMAGE = ubuntu and so on.
for fvInd < len(fv) && patternInd < len(pattern) {
// if we hit marker index, extract its corresponding value
if marker, ok := indices[patternInd]; ok {
// increment the patternInd to end of marker. This helps us to extract the substring before next marker.
patternInd += len(marker)
var value string
if value, fvInd, err = c.extractValueForMarker(fvInd, fv, patternInd, indices); err != nil {
return nil, err
}
// if marker is repeated in the pattern, make sure that the corresponding values
// are same and throw error if not.
if prevValue, ok := m[marker]; ok && prevValue != value {
return nil, errors.Errorf(
"marker %s is found to have different values %s and %s", marker, prevValue, value)
}
m[marker] = value
} else {
// Ex: fv = "samething/ubuntu:0.1.0" pattern = "something/IMAGE:VERSION". Error out at 'a' in fv.
if fv[fvInd] != pattern[patternInd] {
return nil, errors.Errorf(
"unable to derive values for markers, " +
"create setters for all markers and then try again")
}
fvInd++
patternInd++
}
}
// check if both strings are completely visited or throw error
if fvInd < len(fv) || patternInd < len(pattern) {
return nil, errors.Errorf(
"unable to derive values for markers, " +
"create setters for all markers and then try again")
}
return m, nil
}
// GetStartIndices returns the start indices of all the markers in the pattern
func (c SubstitutionCreator) GetStartIndices() (map[int]string, error) {
indices := make(map[int]string)
for _, value := range c.Values {
found := false
for i := range c.Pattern {
if strings.HasPrefix(c.Pattern[i:], value.Marker) {
indices[i] = value.Marker
found = true
}
}
if !found {
return nil, errors.Errorf("unable to find marker " + value.Marker + " in the pattern")
}
}
if err := validateMarkers(indices); err != nil {
return nil, err
}
return indices, nil
}
// validateMarkers takes the indices map, checks if any of 2 markers not have delimiters,
// checks if any marker is substring of other and returns error
func validateMarkers(indices map[int]string) error {
for k1, v1 := range indices {
for k2, v2 := range indices {
if k1 != k2 && k1+len(v1) == k2 {
return errors.Errorf(
"markers %s and %s are found to have no delimiters between them,"+
" pre-create setters and try again", v1, v2)
}
if v1 != v2 && strings.Contains(v1, v2) {
return errors.Errorf(
"markers %s is substring of %s,"+
" no marker should be substring of other", v2, v1)
}
}
}
return nil
}
// extractValueForMarker returns the value string for a marker and the incremented index
func (c SubstitutionCreator) extractValueForMarker(fvInd int, fv string, patternInd int, indices map[int]string) (string, int, error) {
nonMarkerStr := strTillNextMarker(indices, patternInd, c.Pattern)
// return the remaining string of fv till end if patternInd is at end of pattern
if patternInd == len(c.Pattern) {
return fv[fvInd:], len(fv), nil
}
// split remaining fv starting from fvInd with the non marker substring delimiter and get the first value
// In example fv = "something/ubuntu::0.1.0", pattern = "something/IMAGE::VERSION",
// split with "::" delimiter in fv which gives markerValue = ubuntu for marker IMAGE
// increment fvInd by length of extracted marker value and return fvInd
if markerValues := strings.Split(fv[fvInd:], nonMarkerStr); len(markerValues) > 0 {
return markerValues[0], fvInd + len(markerValues[0]), nil
}
return "", -1, errors.Errorf(
"unable to derive values for markers," +
" create setters for all markers and then try again")
}
// substrOfLen takes a string, start index and length and returns substring of given length
// or till end of string
func substrOfLen(str string, startInd int, length int) string {
return str[startInd:min(len(str), startInd+length)]
}
// strTillNextMarker takes in the indices map, a start index and returns the substring till
// start of next marker
func strTillNextMarker(indices map[int]string, startInd int, pattern string) string {
// initialize with max value which is length of pattern
nextMarkerStartInd := len(pattern)
for ind := range indices {
if ind > startInd {
nextMarkerStartInd = min(ind-startInd, nextMarkerStartInd)
}
}
return substrOfLen(pattern, startInd, nextMarkerStartInd)
}
func min(a int, b int) int {
if a < b {
return a
}
return b
}
func (c SubstitutionCreator) validateSubstitutionInfo() error {
// check if substitution with same name exists and throw error
ref, err := spec.NewRef(fieldmeta.DefinitionsPrefix + fieldmeta.SubstitutionDefinitionPrefix + c.Name)
if err != nil {
return err
}
subst, _ := openapi.Resolve(&ref, c.SettersSchema)
// if substitution already exists with the input substitution name, throw error
if subst != nil {
return errors.Errorf("substitution with name %q already exists", c.Name)
}
// check if setter with same name exists and throw error
ref, err = spec.NewRef(fieldmeta.DefinitionsPrefix + fieldmeta.SetterDefinitionPrefix + c.Name)
if err != nil {
return err
}
setter, _ := openapi.Resolve(&ref, c.SettersSchema)
// if setter already exists with input substitution name, throw error
if setter != nil {
return errors.Errorf(fmt.Sprintf("setter with name %q already exists, "+
"substitution and setter can't have same name", c.Name))
}
return nil
}

View File

@@ -1,111 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package settersutil
import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/setters2"
)
func TestGetValuesForMarkers(t *testing.T) {
var tests = []struct {
name string
pattern string
fieldValue string
markers []string
expectedError error
expectedOutput map[string]string
}{
{
name: "positive example",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGE::VERSION/otherthing/IMAGE::VERSION/",
fieldValue: "something/nginx::0.1.0/otherthing/nginx::0.1.0/",
expectedOutput: map[string]string{"IMAGE": "nginx", "VERSION": "0.1.0"},
},
{
name: "marker with different values",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGE:VERSION/IMAGE",
fieldValue: "something/nginx:0.1.0/ubuntu",
expectedError: errors.Errorf("marker IMAGE is found to have different values nginx and ubuntu"),
},
{
name: "unmatched pattern",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGE:VERSION",
fieldValue: "otherthing/nginx:0.1.0",
expectedError: errors.Errorf("unable to derive values for markers"),
},
{
name: "unmatched pattern at the end",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGE:VERSION/abc",
fieldValue: "something/nginx:0.1.0/abcd",
expectedError: errors.Errorf("unable to derive values for markers"),
},
{
name: "substring markers",
markers: []string{"IMAGE", "VERSION", "MAGE"},
pattern: "something/IMAGE:VERSION/abc/MAGE",
fieldValue: "something/nginx:0.1.0/abc/ubuntu",
expectedError: errors.Errorf("no marker should be substring of other"),
},
{
name: "markers with no delimiters",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGEVERSION/",
fieldValue: "something/nginx0.1.0/",
expectedError: errors.Errorf("no delimiters between them"),
},
{
name: "unmatched delimiter",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGE:^VERSION/otherthing/IMAGE::VERSION/",
fieldValue: "something/nginx::0.1.0/otherthing/nginx::0.1.0/",
expectedError: errors.Errorf("unable to derive values for markers"),
},
}
for i := range tests {
test := tests[i]
t.Run(test.name, func(t *testing.T) {
values := []setters2.Value{}
for _, marker := range test.markers {
value := setters2.Value{
Marker: marker,
}
values = append(values, value)
}
sc := SubstitutionCreator{
Pattern: test.pattern,
Values: values,
FieldValue: test.fieldValue,
}
m, err := sc.GetValuesForMarkers()
if test.expectedError == nil {
// fail if expectedError is nil but actual error is not
if !assert.NoError(t, err) {
t.FailNow()
}
// check if all the expected markers and values are present in actual map
for k, v := range test.expectedOutput {
if val, ok := m[k]; ok {
assert.Equal(t, v, val)
} else {
t.FailNow()
}
}
} else {
// if expectedError is not nil, check for correctness of error message
assert.Contains(t, err.Error(), test.expectedError.Error())
}
})
}
}

View File

@@ -1,74 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"encoding/json"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/openapi"
)
type CliExtension struct {
Setter *setter `yaml:"setter,omitempty" json:"setter,omitempty"`
Substitution *substitution `yaml:"substitution,omitempty" json:"substitution,omitempty"`
}
type setter struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Value string `yaml:"value,omitempty" json:"value,omitempty"`
ListValues []string `yaml:"listValues,omitempty" json:"listValues,omitempty"`
EnumValues map[string]string `yaml:"enumValues,omitempty" json:"enumValues,omitempty"`
Required bool `yaml:"required,omitempty" json:"required,omitempty"`
IsSet bool `yaml:"isSet,omitempty" json:"isSet,omitempty"`
}
type substitution struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Pattern string `yaml:"pattern,omitempty" json:"pattern,omitempty"`
Values []substitutionSetterReference `yaml:"values,omitempty" json:"values,omitempty"`
}
type substitutionSetterReference struct {
Ref string `yaml:"ref,omitempty" json:"ref,omitempty"`
Marker string `yaml:"marker,omitempty" json:"marker,omitempty"`
}
// K8sCliExtensionKey is the name of the OpenAPI field containing the setter extensions
const K8sCliExtensionKey = "x-k8s-cli"
// GetExtFromSchema returns the cliExtension openAPI extension if it is present in schema
func GetExtFromSchema(schema *spec.Schema) (*CliExtension, error) {
cep := schema.VendorExtensible.Extensions[K8sCliExtensionKey]
if cep == nil {
return nil, nil
}
b, err := json.Marshal(cep)
if err != nil {
return nil, err
}
val := &CliExtension{}
if err := json.Unmarshal(b, val); err != nil {
return nil, err
}
return val, nil
}
// getExtFromComment returns the cliExtension openAPI extension if it is present as
// a comment on the field.
func getExtFromComment(schema *openapi.ResourceSchema) (*CliExtension, error) {
if schema == nil {
// no schema found
// TODO(pwittrock): should this be an error if it doesn't resolve?
return nil, nil
}
// get the cli extension from the openapi (contains setter information)
ext, err := GetExtFromSchema(schema.Schema)
if err != nil {
return nil, errors.Wrap(err)
}
return ext, nil
}

View File

@@ -1,31 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"strings"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
)
// CheckRequiredSettersSet iterates through all the setter definitions in openAPI
// schema and returns error if any of the setter has required field true and isSet false
func CheckRequiredSettersSet(settersSchema *spec.Schema) error {
for key := range settersSchema.Definitions {
if strings.Contains(key, fieldmeta.SetterDefinitionPrefix) {
val := settersSchema.Definitions[key]
defExt, err := GetExtFromSchema(&val) // parse the extension out of the openAPI
if err != nil {
return errors.Wrap(err)
}
if defExt.Setter != nil && defExt.Setter.Required && !defExt.Setter.IsSet {
return errors.Errorf("setter %s is required but not set, "+
"please set it to new value and try again", strings.TrimPrefix(key, fieldmeta.SetterDefinitionPrefix))
}
}
}
return nil
}

View File

@@ -1,166 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/openapi"
)
func TestCheckRequiredSettersSet(t *testing.T) {
var tests = []struct {
name string
inputOpenAPIfile string
expectedError bool
}{
{
name: "no required, no isSet",
inputOpenAPIfile: `
apiVersion: v1alpha1
kind: OpenAPIfile
openAPI:
definitions:
io.k8s.cli.setters.gcloud.project.projectNumber:
description: hello world
x-k8s-cli:
setter:
name: gcloud.project.projectNumber
value: "123"
setBy: me
io.k8s.cli.setters.replicas:
description: hello world
x-k8s-cli:
setter:
name: replicas
value: "3"
setBy: me
`,
expectedError: false,
},
{
name: "required true, no isSet",
inputOpenAPIfile: `
apiVersion: v1alpha1
kind: Example
openAPI:
definitions:
io.k8s.cli.setters.replicas:
description: hello world
x-k8s-cli:
setter:
name: replicas
value: "3"
setBy: me
required: true
`,
expectedError: true,
},
{
name: "required true, isSet true",
inputOpenAPIfile: `
apiVersion: v1alpha1
kind: Example
openAPI:
definitions:
io.k8s.cli.setters.replicas:
description: hello world
x-k8s-cli:
setter:
name: replicas
value: "3"
setBy: me
required: true
isSet: true
`,
expectedError: false,
},
{
name: "required false, isSet true",
inputOpenAPIfile: `
apiVersion: v1alpha1
kind: OpenAPIfile
openAPI:
definitions:
io.k8s.cli.setters.gcloud.project.projectNumber:
description: hello world
x-k8s-cli:
setter:
name: gcloud.project.projectNumber
value: "123"
setBy: me
io.k8s.cli.setters.replicas:
description: hello world
x-k8s-cli:
setter:
name: replicas
value: "3"
setBy: me
required: false
isSet: true
`,
expectedError: false,
},
{
name: "required true, isSet false",
inputOpenAPIfile: `
apiVersion: v1alpha1
kind: OpenAPIfile
openAPI:
definitions:
io.k8s.cli.setters.gcloud.project.projectNumber:
description: hello world
x-k8s-cli:
setter:
name: gcloud.project.projectNumber
value: "123"
setBy: me
io.k8s.cli.setters.replicas:
description: hello world
x-k8s-cli:
setter:
name: replicas
value: "3"
setBy: me
required: true
isSet: false
`,
expectedError: true,
},
}
for i := range tests {
test := tests[i]
t.Run(test.name, func(t *testing.T) {
openapi.ResetOpenAPI()
defer openapi.ResetOpenAPI()
dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "Krmfile"), []byte(test.inputOpenAPIfile), 0600)
if !assert.NoError(t, err) {
t.FailNow()
}
sc, err := openapi.SchemaFromFile(filepath.Join(dir, "Krmfile"))
if !assert.NoError(t, err) {
t.FailNow()
}
if err != nil {
// do nothing if openAPI file or schema doesn't exist, CheckRequiredSettersSet()
// should not throw any error
fmt.Println("Unable to load schema from file, continuing...")
}
err = CheckRequiredSettersSet(sc)
if test.expectedError && !assert.Error(t, err) {
t.FailNow()
}
if !test.expectedError && !assert.NoError(t, err) {
t.FailNow()
}
})
}
}

View File

@@ -1,124 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// visitor is implemented by structs which need to walk the configuration.
// visitor is provided to accept to walk configuration
type visitor interface {
// visitScalar is called for each scalar field value on a resource
// node is the scalar field value
// path is the path to the field; path elements are separated by '.'
// oa is the OpenAPI schema for the field
visitScalar(node *yaml.RNode, path string, oa *openapi.ResourceSchema, fieldOA *openapi.ResourceSchema) error
// visitSequence is called for each sequence field value on a resource
// node is the sequence field value
// path is the path to the field
// oa is the OpenAPI schema for the field
visitSequence(node *yaml.RNode, path string, oa *openapi.ResourceSchema) error
// visitMapping is called for each Mapping field value on a resource
// node is the mapping field value
// path is the path to the field
// oa is the OpenAPI schema for the field
visitMapping(node *yaml.RNode, path string, oa *openapi.ResourceSchema) error
}
// accept invokes the appropriate function on v for each field in object
func accept(v visitor, object *yaml.RNode, settersSchema *spec.Schema) error {
// get the OpenAPI for the type if it exists
oa := getSchema(object, nil, "", settersSchema)
return acceptImpl(v, object, "", oa, settersSchema)
}
// acceptImpl implements accept using recursion
func acceptImpl(v visitor, object *yaml.RNode, p string, oa *openapi.ResourceSchema, settersSchema *spec.Schema) error {
switch object.YNode().Kind {
case yaml.DocumentNode:
// Traverse the child of the document
return accept(v, yaml.NewRNode(object.YNode()), settersSchema)
case yaml.MappingNode:
if err := v.visitMapping(object, p, oa); err != nil {
return err
}
return object.VisitFields(func(node *yaml.MapNode) error {
// get the schema for the field and propagate it
fieldSchema := getSchema(node.Key, oa, node.Key.YNode().Value, settersSchema)
// Traverse each field value
return acceptImpl(v, node.Value, p+"."+node.Key.YNode().Value, fieldSchema, settersSchema)
})
case yaml.SequenceNode:
// get the schema for the sequence node, use the schema provided if not present
// on the field
if err := v.visitSequence(object, p, oa); err != nil {
return err
}
// get the schema for the elements
schema := getSchema(object, oa, "", settersSchema)
return object.VisitElements(func(node *yaml.RNode) error {
// Traverse each list element
return acceptImpl(v, node, p, schema, settersSchema)
})
case yaml.ScalarNode:
// Visit the scalar field
fieldSchema := getSchema(object, oa, "", settersSchema)
return v.visitScalar(object, p, oa, fieldSchema)
}
return nil
}
// getSchema returns OpenAPI schema for an RNode or field of the
// RNode. It will overriding the provide schema with field specific values
// if they are found
// r is the Node to get the Schema for
// s is the provided schema for the field if known
// field is the name of the field
func getSchema(r *yaml.RNode, s *openapi.ResourceSchema, field string, settersSchema *spec.Schema) *openapi.ResourceSchema {
// get the override schema if it exists on the field
fm := fieldmeta.FieldMeta{SettersSchema: settersSchema}
if err := fm.Read(r); err == nil && !fm.IsEmpty() {
// per-field schema, this is fine
if fm.Schema.Ref.String() != "" {
// resolve the reference
s, err := openapi.Resolve(&fm.Schema.Ref, settersSchema)
if err == nil && s != nil {
fm.Schema = *s
}
}
return &openapi.ResourceSchema{Schema: &fm.Schema}
}
// get the schema for a field of the node if the field is provided
if s != nil && field != "" {
return s.Field(field)
}
// get the schema for the elements if this is a list
if s != nil && r.YNode().Kind == yaml.SequenceNode {
return s.Elements()
}
// use the provided schema if present
if s != nil {
return s
}
if yaml.IsMissingOrNull(r) {
return nil
}
// lookup the schema for the type
m, _ := r.GetMeta()
if m.Kind == "" || m.APIVersion == "" {
return nil
}
return openapi.SchemaForResourceType(yaml.TypeMeta{Kind: m.Kind, APIVersion: m.APIVersion})
}