From ae735282ad547b6057df751ddfa4f8ac3b9d29f7 Mon Sep 17 00:00:00 2001 From: Antoine Pelisse Date: Thu, 4 Oct 2018 15:04:18 -0700 Subject: [PATCH 1/3] Create a method to check dryRun is supported We don't want to run dryRun requests against servers that don't support dry-run, since they might ignore the flag and just persist the unwanted changes. This creates a new method that checks in the OpenAPI if the dryRun parameter can be used. --- pkg/kubectl/cmd/util/openapi/BUILD | 3 + pkg/kubectl/cmd/util/openapi/dryrun.go | 65 +++++++++++++++++ pkg/kubectl/cmd/util/openapi/dryrun_test.go | 80 +++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 pkg/kubectl/cmd/util/openapi/dryrun.go create mode 100644 pkg/kubectl/cmd/util/openapi/dryrun_test.go diff --git a/pkg/kubectl/cmd/util/openapi/BUILD b/pkg/kubectl/cmd/util/openapi/BUILD index 398af66eec3..5958bec309a 100644 --- a/pkg/kubectl/cmd/util/openapi/BUILD +++ b/pkg/kubectl/cmd/util/openapi/BUILD @@ -10,6 +10,7 @@ go_library( name = "go_default_library", srcs = [ "doc.go", + "dryrun.go", "extensions.go", "openapi.go", "openapi_getter.go", @@ -20,6 +21,7 @@ go_library( "//staging/src/k8s.io/client-go/discovery:go_default_library", "//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library", + "//vendor/gopkg.in/yaml.v2:go_default_library", "//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library", ], ) @@ -28,6 +30,7 @@ go_test( name = "go_default_test", size = "small", srcs = [ + "dryrun_test.go", "openapi_getter_test.go", "openapi_suite_test.go", "openapi_test.go", diff --git a/pkg/kubectl/cmd/util/openapi/dryrun.go b/pkg/kubectl/cmd/util/openapi/dryrun.go new file mode 100644 index 00000000000..33cf9e9e5c8 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/dryrun.go @@ -0,0 +1,65 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "errors" + + openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2" + yaml "gopkg.in/yaml.v2" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func hasGVKExtension(extensions []*openapi_v2.NamedAny, gvk schema.GroupVersionKind) bool { + for _, extension := range extensions { + if extension.GetValue().GetYaml() == "" || + extension.GetName() != "x-kubernetes-group-version-kind" { + continue + } + var value map[string]string + err := yaml.Unmarshal([]byte(extension.GetValue().GetYaml()), &value) + if err != nil { + continue + } + + if value["group"] == gvk.Group && value["kind"] == gvk.Kind && value["version"] == gvk.Version { + return true + } + return false + } + return false +} + +// SupportsDryRun is a method that let's us look in the OpenAPI if the +// specific group-version-kind supports the dryRun query parameter for +// the PATCH end-point. +func SupportsDryRun(doc *openapi_v2.Document, gvk schema.GroupVersionKind) (bool, error) { + for _, path := range doc.GetPaths().GetPath() { + // Is this describing the gvk we're looking for? + if !hasGVKExtension(path.GetValue().GetPatch().GetVendorExtension(), gvk) { + continue + } + for _, param := range path.GetValue().GetPatch().GetParameters() { + if param.GetParameter().GetNonBodyParameter().GetQueryParameterSubSchema().GetName() == "dryRun" { + return true, nil + } + } + return false, nil + } + + return false, errors.New("couldn't find GVK in openapi") +} diff --git a/pkg/kubectl/cmd/util/openapi/dryrun_test.go b/pkg/kubectl/cmd/util/openapi/dryrun_test.go new file mode 100644 index 00000000000..e9398945b32 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/dryrun_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi_test + +import ( + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi" +) + +func TestSupportsDryRun(t *testing.T) { + doc, err := fakeSchema.OpenAPISchema() + if err != nil { + t.Fatalf("Failed to get OpenAPI Schema: %v", err) + } + + tests := []struct { + gvk schema.GroupVersionKind + success bool + supports bool + }{ + { + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + success: true, + supports: true, + }, + { + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "UnknownKind", + }, + success: false, + supports: false, + }, + { + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "NodeProxyOptions", + }, + success: true, + supports: false, + }, + } + + for _, test := range tests { + supports, err := openapi.SupportsDryRun(doc, test.gvk) + if supports != test.supports || ((err == nil) != test.success) { + errStr := "nil" + if test.success == false { + errStr = "err" + } + t.Errorf("SupportsDryRun(doc, %v) = (%v, %v), expected (%v, %v)", + test.gvk, + supports, err, + test.supports, errStr, + ) + } + } +} From a0460a5238b4b78b9de956dd7474e61fd9fb2504 Mon Sep 17 00:00:00 2001 From: Antoine Pelisse Date: Mon, 8 Oct 2018 15:09:52 -0700 Subject: [PATCH 2/3] dry-run: Create class to find if a type is a CRD Finding out if a Group-version-kind is a CRD is useful, since we want to detect dry-run ability differently for CRDs. --- pkg/kubectl/cmd/util/BUILD | 7 +- pkg/kubectl/cmd/util/crdfinder.go | 108 +++++++++++++++++++++++++ pkg/kubectl/cmd/util/crdfinder_test.go | 89 ++++++++++++++++++++ 3 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 pkg/kubectl/cmd/util/crdfinder.go create mode 100644 pkg/kubectl/cmd/util/crdfinder_test.go diff --git a/pkg/kubectl/cmd/util/BUILD b/pkg/kubectl/cmd/util/BUILD index 124d14148a2..b57b84e2f9e 100644 --- a/pkg/kubectl/cmd/util/BUILD +++ b/pkg/kubectl/cmd/util/BUILD @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "conversion.go", + "crdfinder.go", "factory.go", "factory_client_access.go", "generator.go", @@ -34,6 +35,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", @@ -57,7 +59,10 @@ go_library( go_test( name = "go_default_test", - srcs = ["helpers_test.go"], + srcs = [ + "crdfinder_test.go", + "helpers_test.go", + ], embed = [":go_default_library"], deps = [ "//pkg/kubectl/scheme:go_default_library", diff --git a/pkg/kubectl/cmd/util/crdfinder.go b/pkg/kubectl/cmd/util/crdfinder.go new file mode 100644 index 00000000000..d3674ae1601 --- /dev/null +++ b/pkg/kubectl/cmd/util/crdfinder.go @@ -0,0 +1,108 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "reflect" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" +) + +// CRDGetter is a function that can download the list of GVK for all +// CRDs. +type CRDGetter func() ([]schema.GroupKind, error) + +func CRDFromDynamic(client dynamic.Interface) CRDGetter { + return func() ([]schema.GroupKind, error) { + list, err := client.Resource(schema.GroupVersionResource{ + Group: "apiextensions.k8s.io", + Version: "v1beta1", + Resource: "curstomresourcedefinitions", + }).List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + if list == nil { + return nil, nil + } + + gks := []schema.GroupKind{} + + // We need to parse the list to get the gvk, I guess that's fine. + for _, crd := range (*list).Items { + // Look for group, version, and kind + group, _, _ := unstructured.NestedString(crd.Object, "spec", "group") + kind, _, _ := unstructured.NestedString(crd.Object, "spec", "names", "kind") + + gks = append(gks, schema.GroupKind{ + Group: group, + Kind: kind, + }) + } + + return gks, nil + } +} + +// CRDFinder keeps a cache of known CRDs and finds a given GVK in the +// list. +type CRDFinder interface { + HasCRD(gvk schema.GroupKind) (bool, error) +} + +func NewCRDFinder(getter CRDGetter) CRDFinder { + return &crdFinder{ + getter: getter, + } +} + +type crdFinder struct { + getter CRDGetter + cache *[]schema.GroupKind +} + +func (f *crdFinder) cacheCRDs() error { + if f.cache != nil { + return nil + } + + list, err := f.getter() + if err != nil { + return err + } + f.cache = &list + return nil +} + +func (f *crdFinder) findCRD(gvk schema.GroupKind) bool { + for _, crd := range *f.cache { + if reflect.DeepEqual(gvk, crd) { + return true + } + } + return false +} + +func (f *crdFinder) HasCRD(gvk schema.GroupKind) (bool, error) { + if err := f.cacheCRDs(); err != nil { + return false, err + } + return f.findCRD(gvk), nil +} diff --git a/pkg/kubectl/cmd/util/crdfinder_test.go b/pkg/kubectl/cmd/util/crdfinder_test.go new file mode 100644 index 00000000000..5c90a0c2568 --- /dev/null +++ b/pkg/kubectl/cmd/util/crdfinder_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util_test + +import ( + "errors" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubernetes/pkg/kubectl/cmd/util" +) + +func TestCacheCRDFinder(t *testing.T) { + called := 0 + getter := func() ([]schema.GroupKind, error) { + called += 1 + return nil, nil + } + finder := util.NewCRDFinder(getter) + if called != 0 { + t.Fatalf("Creating the finder shouldn't call the getter, has called = %v", called) + } + _, err := finder.HasCRD(schema.GroupKind{Group: "", Kind: "Pod"}) + if err != nil { + t.Fatalf("Failed to call HasCRD: %v", err) + } + if called != 1 { + t.Fatalf("First call should call the getter, has called = %v", called) + } + + _, err = finder.HasCRD(schema.GroupKind{Group: "", Kind: "Pod"}) + if err != nil { + t.Fatalf("Failed to call HasCRD: %v", err) + } + if called != 1 { + t.Fatalf("Second call should NOT call the getter, has called = %v", called) + } +} + +func TestCRDFinderErrors(t *testing.T) { + getter := func() ([]schema.GroupKind, error) { + return nil, errors.New("not working") + } + finder := util.NewCRDFinder(getter) + found, err := finder.HasCRD(schema.GroupKind{Group: "", Kind: "Pod"}) + if found == true { + t.Fatalf("Found the CRD with non-working getter function") + } + if err == nil { + t.Fatalf("Error in getter should be reported") + } +} + +func TestCRDFinder(t *testing.T) { + getter := func() ([]schema.GroupKind, error) { + return []schema.GroupKind{ + { + Group: "crd.com", + Kind: "MyCRD", + }, + { + Group: "crd.com", + Kind: "MyNewCRD", + }, + }, nil + } + finder := util.NewCRDFinder(getter) + + if found, _ := finder.HasCRD(schema.GroupKind{Group: "crd.com", Kind: "MyCRD"}); !found { + t.Fatalf("Failed to find CRD MyCRD") + } + if found, _ := finder.HasCRD(schema.GroupKind{Group: "crd.com", Kind: "Random"}); found { + t.Fatalf("Found crd Random that doesn't exist") + } +} From f2e2a930f94a25f7aadfed7be901b2a8bf96587c Mon Sep 17 00:00:00 2001 From: Antoine Pelisse Date: Thu, 4 Oct 2018 17:10:08 -0700 Subject: [PATCH 3/3] dry-run: Verify if the object supports dry-run For each object, first we verify if they have a dryRun parameter in the openapi for the patch verb. If we can't find the object, we assume that CRD will behave like "namespace". So we check if namespace supports dryRun. If it does, then we verify that the resource is a CRD. --- pkg/kubectl/cmd/apply/BUILD | 3 + pkg/kubectl/cmd/apply/apply.go | 97 +++++++++++++++++++++++------ pkg/kubectl/cmd/apply/apply_test.go | 75 +++++++++++++++++++++- pkg/kubectl/cmd/diff/diff.go | 46 +++++++++----- 4 files changed, 187 insertions(+), 34 deletions(-) diff --git a/pkg/kubectl/cmd/apply/BUILD b/pkg/kubectl/cmd/apply/BUILD index a29c7e77e0c..0b16d0c8e91 100644 --- a/pkg/kubectl/cmd/apply/BUILD +++ b/pkg/kubectl/cmd/apply/BUILD @@ -36,6 +36,7 @@ go_library( "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/printers:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource:go_default_library", + "//staging/src/k8s.io/client-go/discovery:go_default_library", "//staging/src/k8s.io/client-go/dynamic:go_default_library", "//vendor/github.com/ghodss/yaml:go_default_library", "//vendor/github.com/golang/glog:go_default_library", @@ -49,6 +50,7 @@ go_test( name = "go_default_test", srcs = ["apply_test.go"], data = [ + "//api/openapi-spec:swagger-spec", "//test/fixtures", ], embed = [":go_default_library"], @@ -71,6 +73,7 @@ go_test( "//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/rest/fake:go_default_library", "//staging/src/k8s.io/client-go/testing:go_default_library", + "//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", ], ) diff --git a/pkg/kubectl/cmd/apply/apply.go b/pkg/kubectl/cmd/apply/apply.go index e1ee8366ac8..8b13130f727 100644 --- a/pkg/kubectl/cmd/apply/apply.go +++ b/pkg/kubectl/cmd/apply/apply.go @@ -42,6 +42,7 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions/printers" "k8s.io/cli-runtime/pkg/genericclioptions/resource" + "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" oapi "k8s.io/kube-openapi/pkg/util/proto" "k8s.io/kubernetes/pkg/kubectl" @@ -76,11 +77,12 @@ type ApplyOptions struct { PruneWhitelist []string ShouldIncludeUninitialized bool - Validator validation.Schema - Builder *resource.Builder - Mapper meta.RESTMapper - DynamicClient dynamic.Interface - OpenAPISchema openapi.Resources + Validator validation.Schema + Builder *resource.Builder + Mapper meta.RESTMapper + DynamicClient dynamic.Interface + DiscoveryClient discovery.DiscoveryInterface + OpenAPISchema openapi.Resources Namespace string EnforceNamespace bool @@ -211,6 +213,11 @@ func (o *ApplyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { return err } + o.DiscoveryClient, err = f.ToDiscoveryClient() + if err != nil { + return err + } + dynamicClient, err := f.DynamicClient() if err != nil { return err @@ -403,19 +410,25 @@ func (o *ApplyOptions) Run() error { fmt.Fprintf(o.ErrOut, warningNoLastAppliedConfigAnnotation, o.cmdBaseName) } + dryRunVerifier := &DryRunVerifier{ + Finder: cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(o.DynamicClient)), + OpenAPIGetter: o.DiscoveryClient, + } + helper := resource.NewHelper(info.Client, info.Mapping) patcher := &Patcher{ - Mapping: info.Mapping, - Helper: helper, - DynamicClient: o.DynamicClient, - Overwrite: o.Overwrite, - BackOff: clockwork.NewRealClock(), - Force: o.DeleteOptions.ForceDeletion, - Cascade: o.DeleteOptions.Cascade, - Timeout: o.DeleteOptions.Timeout, - GracePeriod: o.DeleteOptions.GracePeriod, - ServerDryRun: o.ServerDryRun, - OpenapiSchema: openapiSchema, + Mapping: info.Mapping, + Helper: helper, + DynamicClient: o.DynamicClient, + DryRunVerifier: dryRunVerifier, + Overwrite: o.Overwrite, + BackOff: clockwork.NewRealClock(), + Force: o.DeleteOptions.ForceDeletion, + Cascade: o.DeleteOptions.Cascade, + Timeout: o.DeleteOptions.Timeout, + GracePeriod: o.DeleteOptions.GracePeriod, + ServerDryRun: o.ServerDryRun, + OpenapiSchema: openapiSchema, } patchBytes, patchedObject, err := patcher.Patch(info.Object, modified, info.Source, info.Namespace, info.Name, o.ErrOut) @@ -668,9 +681,10 @@ func (p *Patcher) delete(namespace, name string) error { } type Patcher struct { - Mapping *meta.RESTMapping - Helper *resource.Helper - DynamicClient dynamic.Interface + Mapping *meta.RESTMapping + Helper *resource.Helper + DynamicClient dynamic.Interface + DryRunVerifier *DryRunVerifier Overwrite bool BackOff clockwork.Clock @@ -684,7 +698,52 @@ type Patcher struct { OpenapiSchema openapi.Resources } +// DryRunVerifier verifies if a given group-version-kind supports DryRun +// against the current server. Sending dryRun requests to apiserver that +// don't support it will result in objects being unwillingly persisted. +// +// It reads the OpenAPI to see if the given GVK supports dryRun. If the +// GVK can not be found, we assume that CRDs will have the same level of +// support as "namespaces", and non-CRDs will not be supported. We +// delay the check for CRDs as much as possible though, since it +// requires an extra round-trip to the server. +type DryRunVerifier struct { + Finder cmdutil.CRDFinder + OpenAPIGetter discovery.OpenAPISchemaInterface +} + +// HasSupport verifies if the given gvk supports DryRun. An error is +// returned if it doesn't. +func (v *DryRunVerifier) HasSupport(gvk schema.GroupVersionKind) error { + oapi, err := v.OpenAPIGetter.OpenAPISchema() + if err != nil { + return fmt.Errorf("failed to download openapi: %v", err) + } + supports, err := openapi.SupportsDryRun(oapi, gvk) + if err != nil { + // We assume that we couldn't find the type, then check for namespace: + supports, _ = openapi.SupportsDryRun(oapi, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}) + // If namespace supports dryRun, then we will support dryRun for CRDs only. + if supports { + supports, err = v.Finder.HasCRD(gvk.GroupKind()) + if err != nil { + return fmt.Errorf("failed to check CRD: %v", err) + } + } + } + if !supports { + return fmt.Errorf("%v doesn't support dry-run", gvk) + } + return nil +} + func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) { + if p.ServerDryRun { + if err := p.DryRunVerifier.HasSupport(p.Mapping.GroupVersionKind); err != nil { + return nil, nil, err + } + } + // Serialize the current configuration of the object from the server. current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj) if err != nil { diff --git a/pkg/kubectl/cmd/apply/apply_test.go b/pkg/kubectl/cmd/apply/apply_test.go index 7611640b8c2..8c1631f684e 100644 --- a/pkg/kubectl/cmd/apply/apply_test.go +++ b/pkg/kubectl/cmd/apply/apply_test.go @@ -29,6 +29,7 @@ import ( "strings" "testing" + "github.com/googleapis/gnostic/OpenAPIv2" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" @@ -52,7 +53,7 @@ import ( ) var ( - fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "api", "openapi-spec", "swagger.json")} + fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "..", "api", "openapi-spec", "swagger.json")} testingOpenAPISchemaFns = []func() (openapi.Resources, error){nil, AlwaysErrorOpenAPISchemaFn, openAPISchemaFn} AlwaysErrorOpenAPISchemaFn = func() (openapi.Resources, error) { return nil, errors.New("cannot get openapi spec") @@ -1317,3 +1318,75 @@ func TestForceApply(t *testing.T) { }) } } + +func TestDryRunVerifier(t *testing.T) { + dryRunVerifier := DryRunVerifier{ + Finder: cmdutil.NewCRDFinder(func() ([]schema.GroupKind, error) { + return []schema.GroupKind{ + { + Group: "crd.com", + Kind: "MyCRD", + }, + { + Group: "crd.com", + Kind: "MyNewCRD", + }, + }, nil + }), + OpenAPIGetter: &fakeSchema, + } + + err := dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "NodeProxyOptions"}) + if err == nil { + t.Fatalf("NodeProxyOptions doesn't support dry-run, yet no error found") + } + + err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}) + if err != nil { + t.Fatalf("Pod should support dry-run: %v", err) + } + + err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "MyCRD"}) + if err != nil { + t.Fatalf("MyCRD should support dry-run: %v", err) + } + + err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "Random"}) + if err == nil { + t.Fatalf("Random doesn't support dry-run, yet no error found") + } +} + +type EmptyOpenAPI struct{} + +func (EmptyOpenAPI) OpenAPISchema() (*openapi_v2.Document, error) { + return &openapi_v2.Document{}, nil +} + +func TestDryRunVerifierNoOpenAPI(t *testing.T) { + dryRunVerifier := DryRunVerifier{ + Finder: cmdutil.NewCRDFinder(func() ([]schema.GroupKind, error) { + return []schema.GroupKind{ + { + Group: "crd.com", + Kind: "MyCRD", + }, + { + Group: "crd.com", + Kind: "MyNewCRD", + }, + }, nil + }), + OpenAPIGetter: EmptyOpenAPI{}, + } + + err := dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}) + if err == nil { + t.Fatalf("Pod doesn't support dry-run, yet no error found") + } + + err = dryRunVerifier.HasSupport(schema.GroupVersionKind{Group: "crd.com", Version: "v1", Kind: "MyCRD"}) + if err == nil { + t.Fatalf("MyCRD doesn't support dry-run, yet no error found") + } +} diff --git a/pkg/kubectl/cmd/diff/diff.go b/pkg/kubectl/cmd/diff/diff.go index 5f1ffc0fa6c..5a58d6a8a24 100644 --- a/pkg/kubectl/cmd/diff/diff.go +++ b/pkg/kubectl/cmd/diff/diff.go @@ -226,10 +226,11 @@ type Object interface { // InfoObject is an implementation of the Object interface. It gets all // the information from the Info object. type InfoObject struct { - LocalObj runtime.Object - Info *resource.Info - Encoder runtime.Encoder - OpenAPI openapi.Resources + LocalObj runtime.Object + Info *resource.Info + Encoder runtime.Encoder + OpenAPI openapi.Resources + DryRunVerifier *apply.DryRunVerifier } var _ Object = &InfoObject{} @@ -261,12 +262,13 @@ func (obj InfoObject) Merged() (runtime.Object, error) { // This is using the patcher from apply, to keep the same behavior. // We plan on replacing this with server-side apply when it becomes available. patcher := &apply.Patcher{ - Mapping: obj.Info.Mapping, - Helper: resource.NewHelper(obj.Info.Client, obj.Info.Mapping), - Overwrite: true, - BackOff: clockwork.NewRealClock(), - ServerDryRun: true, - OpenapiSchema: obj.OpenAPI, + DryRunVerifier: obj.DryRunVerifier, + Mapping: obj.Info.Mapping, + Helper: resource.NewHelper(obj.Info.Client, obj.Info.Mapping), + Overwrite: true, + BackOff: clockwork.NewRealClock(), + ServerDryRun: true, + OpenapiSchema: obj.OpenAPI, } _, result, err := patcher.Patch(obj.Info.Object, modified, obj.Info.Source, obj.Info.Namespace, obj.Info.Name, nil) @@ -330,6 +332,21 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { return err } + discovery, err := f.ToDiscoveryClient() + if err != nil { + return err + } + + dynamic, err := f.DynamicClient() + if err != nil { + return err + } + + dryRunVerifier := &apply.DryRunVerifier{ + Finder: cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(dynamic)), + OpenAPIGetter: discovery, + } + differ, err := NewDiffer("LIVE", "MERGED") if err != nil { return err @@ -367,10 +384,11 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { } obj := InfoObject{ - LocalObj: local, - Info: info, - Encoder: scheme.DefaultJSONEncoder(), - OpenAPI: schema, + LocalObj: local, + Info: info, + Encoder: scheme.DefaultJSONEncoder(), + OpenAPI: schema, + DryRunVerifier: dryRunVerifier, } return differ.Diff(obj, printer)