diff --git a/api/go.mod b/api/go.mod index c3cf13b4a..1df18d7a9 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/evanphx/json-patch v4.5.0+incompatible + github.com/go-errors/errors v1.0.1 github.com/go-openapi/spec v0.19.5 github.com/golangci/golangci-lint v1.21.0 github.com/google/go-cmp v0.3.0 diff --git a/api/internal/wrappy/factory.go b/api/internal/wrappy/factory.go index 4bce50602..125afdb96 100644 --- a/api/internal/wrappy/factory.go +++ b/api/internal/wrappy/factory.go @@ -6,7 +6,9 @@ package wrappy import ( "bytes" "fmt" + "sort" + "github.com/go-errors/errors" "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/types" @@ -76,11 +78,107 @@ func (k *WNodeFactory) Hasher() ifc.KunstructuredHasher { } func (k *WNodeFactory) MakeConfigMap( - kvLdr ifc.KvLoader, args *types.ConfigMapArgs) (ifc.Kunstructured, error) { - panic("TODO(#WNodeFactory): implement MakeConfigMap") + ldr ifc.KvLoader, args *types.ConfigMapArgs) (ifc.Kunstructured, error) { + rn, err := k.makeConfigMap(ldr, args) + if err != nil { + return nil, err + } + return FromRNode(rn), nil +} + +func (k *WNodeFactory) makeConfigMap( + ldr ifc.KvLoader, args *types.ConfigMapArgs) (*yaml.RNode, error) { + rn, err := yaml.Parse(` +apiVersion: v1 +kind: ConfigMap +`) + if err != nil { + return nil, err + } + err = applyGeneratorArgs(rn, ldr, args.GeneratorArgs) + return rn, err } func (k *WNodeFactory) MakeSecret( - kvLdr ifc.KvLoader, args *types.SecretArgs) (ifc.Kunstructured, error) { - panic("TODO(#WNodeFactory): implement MakeSecret") + ldr ifc.KvLoader, args *types.SecretArgs) (ifc.Kunstructured, error) { + rn, err := k.makeSecret(ldr, args) + if err != nil { + return nil, err + } + return FromRNode(rn), nil +} + +func (k *WNodeFactory) makeSecret( + ldr ifc.KvLoader, args *types.SecretArgs) (*yaml.RNode, error) { + rn, err := yaml.Parse(` +apiVersion: v1 +kind: Secret +`) + if err != nil { + return nil, err + } + err = applyGeneratorArgs(rn, ldr, args.GeneratorArgs) + if 1+1 == 2 { + err = fmt.Errorf("TODO(WNodeFactory): finish implementation of makeSecret") + } + return rn, err +} + +func applyGeneratorArgs( + rn *yaml.RNode, ldr ifc.KvLoader, args types.GeneratorArgs) error { + if _, err := rn.Pipe(yaml.SetK8sName(args.Name)); err != nil { + return err + } + if args.Namespace != "" { + if _, err := rn.Pipe(yaml.SetK8sNamespace(args.Namespace)); err != nil { + return err + } + } + all, err := ldr.Load(args.KvPairSources) + if err != nil { + return errors.WrapPrefix(err, "loading KV pairs", 0) + } + for _, p := range all { + if err := ldr.Validator().ErrIfInvalidKey(p.Key); err != nil { + return err + } + if _, err := rn.Pipe(yaml.SetK8sData(p.Key, p.Value)); err != nil { + return errors.WrapPrefix(err, "configMap generate error", 0) + } + } + copyLabelsAndAnnotations(rn, args.Options) + return nil +} + +// copyLabelsAndAnnotations copies labels and annotations from +// GeneratorOptions into the given object. +func copyLabelsAndAnnotations( + rn *yaml.RNode, opts *types.GeneratorOptions) error { + if opts == nil { + return nil + } + for _, k := range sortedKeys(opts.Labels) { + v := opts.Labels[k] + if _, err := rn.Pipe(yaml.SetLabel(k, v)); err != nil { + return err + } + } + for _, k := range sortedKeys(opts.Annotations) { + v := opts.Annotations[k] + if _, err := rn.Pipe(yaml.SetAnnotation(k, v)); err != nil { + return err + } + } + return nil +} + +func sortedKeys(m map[string]string) []string { + keys := make([]string, len(m)) + i := 0 + for k := range m { + keys[i] = k + i++ + } + sort.Strings(keys) + return keys } diff --git a/api/internal/wrappy/factory_test.go b/api/internal/wrappy/factory_test.go index 77dee3bae..c4b464822 100644 --- a/api/internal/wrappy/factory_test.go +++ b/api/internal/wrappy/factory_test.go @@ -5,10 +5,395 @@ package wrappy import ( "fmt" + "path/filepath" "reflect" "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/api/filesys" + "sigs.k8s.io/kustomize/api/kv" + "sigs.k8s.io/kustomize/api/loader" + valtest_test "sigs.k8s.io/kustomize/api/testutils/valtest" + "sigs.k8s.io/kustomize/api/types" ) +func TestMakeConfigMap(t *testing.T) { + factory := &WNodeFactory{} + type expected struct { + out string + errMsg string + } + + testCases := map[string]struct { + args types.ConfigMapArgs + exp expected + }{ + "construct config map from env": { + args: types.ConfigMapArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "envConfigMap", + KvPairSources: types.KvPairSources{ + EnvSources: []string{ + filepath.Join("configmap", "app.env"), + }, + }, + }, + }, + exp: expected{ + out: `apiVersion: v1 +kind: ConfigMap +metadata: + name: envConfigMap +data: + DB_USERNAME: admin + DB_PASSWORD: qwerty +`, + }, + }, + "construct config map from text file": { + args: types.ConfigMapArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "fileConfigMap1", + KvPairSources: types.KvPairSources{ + FileSources: []string{ + filepath.Join("configmap", "app-init.ini"), + }, + }, + }, + }, + exp: expected{ + out: `apiVersion: v1 +kind: ConfigMap +metadata: + name: fileConfigMap1 +data: + app-init.ini: | + FOO=bar + BAR=baz +`, + }, + }, + "construct config map from text and binary file": { + args: types.ConfigMapArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "fileConfigMap2", + KvPairSources: types.KvPairSources{ + FileSources: []string{ + filepath.Join("configmap", "app-init.ini"), + filepath.Join("configmap", "app.bin"), + }, + }, + }, + }, + exp: expected{ + errMsg: "configMap generate error: key 'app.bin' appears " + + "to have non-utf8 data; binaryData field not yet supported", + out: `apiVersion: v1 +kind: ConfigMap +metadata: + name: fileConfigMap2 +data: + app-init.ini: | + FOO=bar + BAR=baz +`, + }, + }, + "construct config map from literal": { + args: types.ConfigMapArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "literalConfigMap1", + KvPairSources: types.KvPairSources{ + LiteralSources: []string{"a=x", "b=y", "c=\"Hello World\"", "d='true'"}, + }, + Options: &types.GeneratorOptions{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + exp: expected{ + out: `apiVersion: v1 +kind: ConfigMap +metadata: + name: literalConfigMap1 + labels: + foo: 'bar' +data: + a: x + b: y + c: Hello World + d: "true" +`, + }, + }, + "construct config map from literal with GeneratorOptions in ConfigMapArgs": { + args: types.ConfigMapArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "literalConfigMap2", + KvPairSources: types.KvPairSources{ + LiteralSources: []string{"a=x", "b=y", "c=\"Hello World\"", "d='true'"}, + }, + Options: &types.GeneratorOptions{ + Labels: map[string]string{ + "veggie": "celery", + "dog": "beagle", + "cat": "annoying", + }, + Annotations: map[string]string{ + "river": "Missouri", + "city": "Iowa City", + }, + }, + }, + }, + exp: expected{ + out: `apiVersion: v1 +kind: ConfigMap +metadata: + name: literalConfigMap2 + labels: + cat: 'annoying' + dog: 'beagle' + veggie: 'celery' + annotations: + city: 'Iowa City' + river: 'Missouri' +data: + a: x + b: y + c: Hello World + d: "true" +`, + }, + }, + } + fSys := filesys.MakeFsInMemory() + fSys.WriteFile( + filesys.RootedPath("configmap", "app.env"), + []byte("DB_USERNAME=admin\nDB_PASSWORD=qwerty\n")) + fSys.WriteFile( + filesys.RootedPath("configmap", "app-init.ini"), + []byte("FOO=bar\nBAR=baz\n")) + fSys.WriteFile( + filesys.RootedPath("configmap", "app.bin"), + []byte{0xff, 0xfd}) + kvLdr := kv.NewLoader( + loader.NewFileLoaderAtRoot(fSys), + valtest_test.MakeFakeValidator()) + + for n := range testCases { + tc := testCases[n] + t.Run(n, func(t *testing.T) { + rn, err := factory.makeConfigMap(kvLdr, &tc.args) + if err != nil { + if !assert.EqualError(t, err, tc.exp.errMsg) { + t.FailNow() + } + return + } + if tc.exp.errMsg != "" { + t.Fatalf("%s: should return error '%s'", n, tc.exp.errMsg) + } + output := rn.MustString() + if !assert.Equal(t, tc.exp.out, output) { + t.FailNow() + } + }) + } +} + +func TestMakeSecret(t *testing.T) { + factory := &WNodeFactory{} + type expected struct { + out string + errMsg string + } + + testCases := map[string]struct { + args types.SecretArgs + exp expected + }{ + "construct secret from env": { + args: types.SecretArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "envSecret", + KvPairSources: types.KvPairSources{ + EnvSources: []string{ + filepath.Join("secret", "app.env"), + }, + }, + }, + }, + exp: expected{ + errMsg: "TODO(WNodeFactory): finish implementation of makeSecret", + out: `apiVersion: v1 +kind: Secret +metadata: + name: envSecret +data: + DB_USERNAME: admin + DB_PASSWORD: qwerty +`, + }, + }, + "construct secret from text file": { + args: types.SecretArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "fileSecret1", + KvPairSources: types.KvPairSources{ + FileSources: []string{ + filepath.Join("secret", "app-init.ini"), + }, + }, + }, + }, + exp: expected{ + errMsg: "TODO(WNodeFactory): finish implementation of makeSecret", + out: `apiVersion: v1 +kind: Secret +metadata: + name: fileSecret1 +data: + app-init.ini: | + FOO=bar + BAR=baz +`, + }, + }, + "construct secret from text and binary file": { + args: types.SecretArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "fileSecret2", + KvPairSources: types.KvPairSources{ + FileSources: []string{ + filepath.Join("secret", "app-init.ini"), + filepath.Join("secret", "app.bin"), + }, + }, + }, + }, + exp: expected{ + errMsg: "TODO(WNodeFactory): finish implementation of makeSecret", + out: `apiVersion: v1 +kind: Secret +metadata: + name: fileSecret2 +data: + app-init.ini: | + FOO=bar + BAR=baz +`, + }, + }, + "construct secret from literal": { + args: types.SecretArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "literalSecret1", + KvPairSources: types.KvPairSources{ + LiteralSources: []string{"a=x", "b=y", "c=\"Hello World\"", "d='true'"}, + }, + Options: &types.GeneratorOptions{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + exp: expected{ + errMsg: "TODO(WNodeFactory): finish implementation of makeSecret", + out: `apiVersion: v1 +kind: Secret +metadata: + name: literalSecret1 + labels: + foo: 'bar' +data: + a: x + b: y + c: Hello World + d: "true" +`, + }, + }, + "construct secret from literal with GeneratorOptions in SecretArgs": { + args: types.SecretArgs{ + GeneratorArgs: types.GeneratorArgs{ + Name: "literalSecret2", + KvPairSources: types.KvPairSources{ + LiteralSources: []string{"a=x", "b=y", "c=\"Hello World\"", "d='true'"}, + }, + Options: &types.GeneratorOptions{ + Labels: map[string]string{ + "veggie": "celery", + "dog": "beagle", + "cat": "annoying", + }, + Annotations: map[string]string{ + "river": "Missouri", + "city": "Iowa City", + }, + }, + }, + }, + exp: expected{ + errMsg: "TODO(WNodeFactory): finish implementation of makeSecret", + out: `apiVersion: v1 +kind: Secret +metadata: + name: literalSecret2 + labels: + cat: 'annoying' + dog: 'beagle' + veggie: 'celery' + annotations: + city: 'Iowa City' + river: 'Missouri' +data: + a: x + b: y + c: Hello World + d: "true" +`, + }, + }, + } + fSys := filesys.MakeFsInMemory() + fSys.WriteFile( + filesys.RootedPath("secret", "app.env"), + []byte("DB_USERNAME=admin\nDB_PASSWORD=qwerty\n")) + fSys.WriteFile( + filesys.RootedPath("secret", "app-init.ini"), + []byte("FOO=bar\nBAR=baz\n")) + fSys.WriteFile( + filesys.RootedPath("secret", "app.bin"), + []byte{0xff, 0xfd}) + kvLdr := kv.NewLoader( + loader.NewFileLoaderAtRoot(fSys), + valtest_test.MakeFakeValidator()) + + for n := range testCases { + tc := testCases[n] + t.Run(n, func(t *testing.T) { + rn, err := factory.makeSecret(kvLdr, &tc.args) + if err != nil { + if !assert.EqualError(t, err, tc.exp.errMsg) { + t.FailNow() + } + return + } + if tc.exp.errMsg != "" { + t.Fatalf("%s: should return error '%s'", n, tc.exp.errMsg) + } + output := rn.MustString() + if !assert.Equal(t, tc.exp.out, output) { + t.FailNow() + } + }) + } +} + func TestSliceFromBytes(t *testing.T) { factory := &WNodeFactory{} testConfigMap := diff --git a/kyaml/yaml/kfns.go b/kyaml/yaml/kfns.go index 69e8eb94f..74d467808 100644 --- a/kyaml/yaml/kfns.go +++ b/kyaml/yaml/kfns.go @@ -58,7 +58,7 @@ func (s k8sDataSetter) Filter(rn *RNode) (*RNode, error) { // test in a mapping field called "data" as a string. Pairs with a 'v' // failing this test go into a field called binaryData as a []byte. // TODO: support this distinction in kyaml with NodeTagBytes? - return nil, fmt.Errorf( + return nil, errors.Errorf( "key '%s' appears to have non-utf8 data; "+ "binaryData field not yet supported", s.Key) } @@ -73,8 +73,8 @@ func (s k8sDataSetter) Filter(rn *RNode) (*RNode, error) { } v := NewScalarRNode(s.Value) v.YNode().Tag = NodeTagString - // Add quotes? - // v.YNode().Style = yaml.SingleQuotedStyle + // TODO: use schema to determine node style and tag. + // FormatNonStringStyle(v.YNode(), *k8sSch) _, err = rn.Pipe( LookupCreate(yaml.MappingNode, DataField), SetField(s.Key, v)) return rn, err diff --git a/kyaml/yaml/kfns_test.go b/kyaml/yaml/kfns_test.go index 8bb18319b..2997bfdae 100644 --- a/kyaml/yaml/kfns_test.go +++ b/kyaml/yaml/kfns_test.go @@ -39,8 +39,8 @@ data: fruit: apple veggie: celery ` - if output != expected { - t.Fatalf("expected \n%s\nbut got \n%s\n", expected, output) + if !assert.Equal(t, expected, output) { + t.FailNow() } } @@ -79,8 +79,8 @@ metadata: name: foo namespace: bar ` - if output != expected { - t.Fatalf("expected \n%s\nbut got \n%s\n", expected, output) + if !assert.Equal(t, expected, output) { + t.FailNow() } }