diff --git a/staging/src/k8s.io/cli-runtime/artifacts/kustomization/configMap.yaml b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/configMap.yaml new file mode 100644 index 0000000000..e335ab8cc8 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/configMap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: the-map +data: + altGreeting: "Good Morning!" + enableRisky: "false" diff --git a/staging/src/k8s.io/cli-runtime/artifacts/kustomization/deployment.yaml b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/deployment.yaml new file mode 100644 index 0000000000..6e79409080 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/deployment.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: the-deployment +spec: + replicas: 3 + template: + metadata: + labels: + deployment: hello + spec: + containers: + - name: the-container + image: monopole/hello:1 + command: ["/hello", + "--port=8080", + "--enableRiskyFeature=$(ENABLE_RISKY)"] + ports: + - containerPort: 8080 + env: + - name: ALT_GREETING + valueFrom: + configMapKeyRef: + name: the-map + key: altGreeting + - name: ENABLE_RISKY + valueFrom: + configMapKeyRef: + name: the-map + key: enableRisky diff --git a/staging/src/k8s.io/cli-runtime/artifacts/kustomization/kustomization.yaml b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/kustomization.yaml new file mode 100644 index 0000000000..eef3195711 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/kustomization.yaml @@ -0,0 +1,5 @@ +nameprefix: test- +resources: +- deployment.yaml +- service.yaml +- configMap.yaml diff --git a/staging/src/k8s.io/cli-runtime/artifacts/kustomization/service.yaml b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/service.yaml new file mode 100644 index 0000000000..e238f70021 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/service.yaml @@ -0,0 +1,12 @@ +kind: Service +apiVersion: v1 +metadata: + name: the-service +spec: + selector: + deployment: hello + type: LoadBalancer + ports: + - protocol: TCP + port: 8666 + targetPort: 8080 diff --git a/staging/src/k8s.io/cli-runtime/artifacts/kustomization/should-not-create.yaml b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/should-not-create.yaml new file mode 100644 index 0000000000..a927e6b98b --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/should-not-create.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: should-not-create-map +data: + altGreeting: "Good Morning!" + enableRisky: "false" diff --git a/staging/src/k8s.io/cli-runtime/artifacts/kustomization/should-not-load.yaml b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/should-not-load.yaml new file mode 100644 index 0000000000..f9be4c33a1 --- /dev/null +++ b/staging/src/k8s.io/cli-runtime/artifacts/kustomization/should-not-load.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +nameprefix: test- +resources: +- deployment.yaml +- service.yaml +- configMap.yaml diff --git a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/filename_flags.go b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/filename_flags.go index 348a9c6368..0d43c7808a 100644 --- a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/filename_flags.go +++ b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/filename_flags.go @@ -32,6 +32,7 @@ type FileNameFlags struct { Usage string Filenames *[]string + Kustomize *string Recursive *bool } @@ -48,6 +49,9 @@ func (o *FileNameFlags) ToOptions() resource.FilenameOptions { if o.Filenames != nil { options.Filenames = *o.Filenames } + if o.Kustomize != nil { + options.Kustomize = *o.Kustomize + } return options } @@ -68,4 +72,8 @@ func (o *FileNameFlags) AddFlags(flags *pflag.FlagSet) { } flags.SetAnnotation("filename", cobra.BashCompFilenameExt, annotations) } + if o.Kustomize != nil { + flags.StringVarP(o.Kustomize, "kustomize", "k", *o.Kustomize, + "Process a kustomization directory. This flag can't be used together with -f or -R.") + } } diff --git a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/builder.go b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/builder.go index f3f8173a9c..ad5247e9b4 100644 --- a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/builder.go +++ b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/builder.go @@ -130,9 +130,28 @@ func IsUsageError(err error) bool { type FilenameOptions struct { Filenames []string + Kustomize string Recursive bool } +func (o *FilenameOptions) validate() []error { + var errs []error + if len(o.Filenames) > 0 && len(o.Kustomize) > 0 { + errs = append(errs, fmt.Errorf("only one of -f or -k can be specified")) + } + if len(o.Kustomize) > 0 && o.Recursive { + errs = append(errs, fmt.Errorf("-R is not allowed to work with -k")) + } + return errs +} + +func (o *FilenameOptions) RequireFilenameOrKustomize() error { + if len(o.Filenames) == 0 && len(o.Kustomize) == 0 { + return fmt.Errorf("must specify one of -f and -k") + } + return nil +} + type resourceTuple struct { Resource string Name string @@ -195,6 +214,10 @@ func (b *Builder) AddError(err error) *Builder { // If ContinueOnError() is set prior to this method, objects on the path that are not // recognized will be ignored (but logged at V(2)). func (b *Builder) FilenameParam(enforceNamespace bool, filenameOptions *FilenameOptions) *Builder { + if errs := filenameOptions.validate(); len(errs) > 0 { + b.errs = append(b.errs, errs...) + return b + } recursive := filenameOptions.Recursive paths := filenameOptions.Filenames for _, s := range paths { @@ -215,6 +238,10 @@ func (b *Builder) FilenameParam(enforceNamespace bool, filenameOptions *Filename b.Path(recursive, s) } } + if filenameOptions.Kustomize != "" { + b.paths = append(b.paths, &KustomizeVisitor{filenameOptions.Kustomize, + NewStreamVisitor(nil, b.mapper, filenameOptions.Kustomize, b.schema)}) + } if enforceNamespace { b.RequireNamespace() diff --git a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/builder_test.go b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/builder_test.go index 8aaba3ca22..90b417b282 100644 --- a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/builder_test.go +++ b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/builder_test.go @@ -374,6 +374,62 @@ func writeTestFile(t *testing.T, path string, contents string) { } } +func TestFilenameOptionsValidate(t *testing.T) { + testcases := []struct { + filenames []string + kustomize string + recursive bool + errExp bool + msgExp string + }{ + { + filenames: []string{"file"}, + kustomize: "dir", + errExp: true, + msgExp: "only one of -f or -k can be specified", + }, + { + kustomize: "dir", + recursive: true, + errExp: true, + msgExp: "-R is not allowed to work with -k", + }, + { + filenames: []string{"file"}, + errExp: false, + }, + { + filenames: []string{"dir"}, + recursive: true, + errExp: false, + }, + { + kustomize: "dir", + errExp: false, + }, + } + for _, testcase := range testcases { + o := &FilenameOptions{ + Kustomize: testcase.kustomize, + Filenames: testcase.filenames, + Recursive: testcase.recursive, + } + errs := o.validate() + if testcase.errExp { + if len(errs) == 0 { + t.Fatalf("expected error not happened") + } + if errs[0].Error() != testcase.msgExp { + t.Fatalf("expected %s, but got %#v", testcase.msgExp, errs[0]) + } + } else { + if len(errs) > 0 { + t.Fatalf("Unexpected error %#v", errs) + } + } + } +} + func TestPathBuilderWithMultiple(t *testing.T) { // create test dirs tmpDir, err := utiltesting.MkTmpdir("recursive_test_multiple") @@ -513,6 +569,70 @@ func TestDirectoryBuilder(t *testing.T) { } } +func TestKustomizeDirectoryBuilder(t *testing.T) { + tests := []struct { + directory string + expectErr bool + errMsg string + number int + expectedNames []string + }{ + { + directory: "../../../artifacts/guestbook", + expectErr: true, + errMsg: "No kustomization file found", + }, + // TODO(Liujingfang1): Fix this test in bazel test + //{ + // directory: "../../../artifacts/kustomization", + // expectErr: false, + // expectedNames: []string{"test-the-map", "test-the-deployment", "test-the-service"}, + //}, + { + directory: "../../../artifacts/kustomization/should-not-load.yaml", + expectErr: true, + errMsg: "must be a directory to be a root", + }, + } + for _, tt := range tests { + b := newDefaultBuilder(). + FilenameParam(false, &FilenameOptions{Kustomize: tt.directory}). + NamespaceParam("test").DefaultNamespace() + test := &testVisitor{} + err := b.Do().Visit(test.Handle) + if tt.expectErr { + if err == nil { + t.Fatalf("expected error unhappened") + } + if !strings.Contains(err.Error(), tt.errMsg) { + t.Fatalf("expected %s but got %s", tt.errMsg, err.Error()) + } + } else { + if err != nil || len(test.Infos) < tt.number { + t.Fatalf("unexpected response: %v %#v", err, test.Infos) + } + contained := func(name string) bool { + for _, info := range test.Infos { + if info.Name == name && info.Namespace == "test" && info.Object != nil { + return true + } + } + return false + } + + allFound := true + for _, name := range tt.expectedNames { + if !contained(name) { + allFound = false + } + } + if !allFound { + t.Errorf("unexpected responses: %#v", test.Infos) + } + } + } +} + func TestNamespaceOverride(t *testing.T) { s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK)