diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go b/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go index fe39617852a..ecdcf87c7b8 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/apply/apply.go @@ -22,6 +22,7 @@ import ( "net/http" "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -42,6 +43,7 @@ import ( "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/openapi" + "k8s.io/kubectl/pkg/util/prune" "k8s.io/kubectl/pkg/util/templates" "k8s.io/kubectl/pkg/validation" ) @@ -60,7 +62,7 @@ type ApplyFlags struct { FieldManager string Selector string Prune bool - PruneResources []pruneResource + PruneResources []prune.Resource All bool Overwrite bool OpenAPIPatch bool @@ -85,7 +87,7 @@ type ApplyOptions struct { DryRunStrategy cmdutil.DryRunStrategy DryRunVerifier *resource.DryRunVerifier Prune bool - PruneResources []pruneResource + PruneResources []prune.Resource cmdBaseName string All bool Overwrite bool @@ -278,7 +280,7 @@ func (flags *ApplyFlags) ToOptions(cmd *cobra.Command, baseName string, args []s } if flags.Prune { - flags.PruneResources, err = parsePruneResources(mapper, flags.PruneWhitelist) + flags.PruneResources, err = prune.ParseResources(mapper, flags.PruneWhitelist) if err != nil { return nil, err } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/apply/prune.go b/staging/src/k8s.io/kubectl/pkg/cmd/apply/prune.go index 1b65a3eb9cb..04ce54e5685 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/apply/prune.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/apply/prune.go @@ -20,16 +20,15 @@ import ( "context" "fmt" "io" - "strings" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/printers" "k8s.io/client-go/dynamic" cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/prune" ) type pruner struct { @@ -71,7 +70,7 @@ func newPruner(o *ApplyOptions) pruner { func (p *pruner) pruneAll(o *ApplyOptions) error { - namespacedRESTMappings, nonNamespacedRESTMappings, err := getRESTMappings(o.Mapper, &(o.PruneResources)) + namespacedRESTMappings, nonNamespacedRESTMappings, err := prune.GetRESTMappings(o.Mapper, o.PruneResources) if err != nil { return fmt.Errorf("error retrieving RESTMappings to prune: %v", err) } @@ -158,83 +157,3 @@ func asDeleteOptions(cascadingStrategy metav1.DeletionPropagation, gracePeriod i options.PropagationPolicy = &cascadingStrategy return options } - -type pruneResource struct { - group string - version string - kind string - namespaced bool -} - -func (pr pruneResource) String() string { - return fmt.Sprintf("%v/%v, Kind=%v, Namespaced=%v", pr.group, pr.version, pr.kind, pr.namespaced) -} - -func getRESTMappings(mapper meta.RESTMapper, pruneResources *[]pruneResource) (namespaced, nonNamespaced []*meta.RESTMapping, err error) { - if len(*pruneResources) == 0 { - // default allowlist - *pruneResources = []pruneResource{ - {"", "v1", "ConfigMap", true}, - {"", "v1", "Endpoints", true}, - {"", "v1", "Namespace", false}, - {"", "v1", "PersistentVolumeClaim", true}, - {"", "v1", "PersistentVolume", false}, - {"", "v1", "Pod", true}, - {"", "v1", "ReplicationController", true}, - {"", "v1", "Secret", true}, - {"", "v1", "Service", true}, - {"batch", "v1", "Job", true}, - {"batch", "v1", "CronJob", true}, - {"networking.k8s.io", "v1", "Ingress", true}, - {"apps", "v1", "DaemonSet", true}, - {"apps", "v1", "Deployment", true}, - {"apps", "v1", "ReplicaSet", true}, - {"apps", "v1", "StatefulSet", true}, - } - } - - for _, resource := range *pruneResources { - addedMapping, err := mapper.RESTMapping(schema.GroupKind{Group: resource.group, Kind: resource.kind}, resource.version) - if err != nil { - return nil, nil, fmt.Errorf("invalid resource %v: %v", resource, err) - } - if resource.namespaced { - namespaced = append(namespaced, addedMapping) - } else { - nonNamespaced = append(nonNamespaced, addedMapping) - } - } - - return namespaced, nonNamespaced, nil -} - -func parsePruneResources(mapper meta.RESTMapper, gvks []string) ([]pruneResource, error) { - pruneResources := []pruneResource{} - for _, groupVersionKind := range gvks { - gvk := strings.Split(groupVersionKind, "/") - if len(gvk) != 3 { - return nil, fmt.Errorf("invalid GroupVersionKind format: %v, please follow ", groupVersionKind) - } - - if gvk[0] == "core" { - gvk[0] = "" - } - mapping, err := mapper.RESTMapping(schema.GroupKind{Group: gvk[0], Kind: gvk[2]}, gvk[1]) - if err != nil { - return pruneResources, err - } - var namespaced bool - namespaceScope := mapping.Scope.Name() - switch namespaceScope { - case meta.RESTScopeNameNamespace: - namespaced = true - case meta.RESTScopeNameRoot: - namespaced = false - default: - return pruneResources, fmt.Errorf("Unknown namespace scope: %q", namespaceScope) - } - - pruneResources = append(pruneResources, pruneResource{gvk[0], gvk[1], gvk[2], namespaced}) - } - return pruneResources, nil -} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/diff/diff.go b/staging/src/k8s.io/kubectl/pkg/cmd/diff/diff.go index 5c47744389b..f2a1d439e52 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/diff/diff.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/diff/diff.go @@ -44,6 +44,7 @@ import ( "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/openapi" + "k8s.io/kubectl/pkg/util/prune" "k8s.io/kubectl/pkg/util/templates" "k8s.io/utils/exec" "sigs.k8s.io/yaml" @@ -116,6 +117,7 @@ type DiffOptions struct { EnforceNamespace bool Builder *resource.Builder Diff *DiffProgram + pruner *pruner } func validateArgs(cmd *cobra.Command, args []string) error { @@ -170,6 +172,8 @@ func NewCmdDiff(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C usage := "contains the configuration to diff" cmd.Flags().StringVarP(&options.Selector, "selector", "l", options.Selector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") + cmd.Flags().StringArray("prune-allowlist", []string{}, "Overwrite the default whitelist with for --prune") + cmd.Flags().Bool("prune", false, "Include resources that would be deleted by pruning. Can be used with -l and default shows all resources would be pruned") cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) cmdutil.AddServerSideApplyFlags(cmd) cmdutil.AddFieldManagerFlagVar(cmd, &options.FieldManager, apply.FieldManagerClientSideApply) @@ -642,6 +646,19 @@ func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { return err } + if cmdutil.GetFlagBool(cmd, "prune") { + mapper, err := f.ToRESTMapper() + if err != nil { + return err + } + + resources, err := prune.ParseResources(mapper, cmdutil.GetFlagStringArray(cmd, "prune-allowlist")) + if err != nil { + return err + } + o.pruner = newPruner(o.DynamicClient, mapper, resources) + } + o.Builder = f.NewBuilder() return nil } @@ -707,6 +724,10 @@ func (o *DiffOptions) Run() error { IOStreams: o.Diff.IOStreams, } + if o.pruner != nil { + o.pruner.MarkVisited(info) + } + err = differ.Diff(obj, printer) if !isConflict(err) { break @@ -717,9 +738,52 @@ func (o *DiffOptions) Run() error { return err }) + + if o.pruner != nil { + prunedObjs, err := o.pruner.pruneAll() + if err != nil { + klog.Warningf("pruning failed and could not be evaluated err: %v", err) + } + + // Print pruned objects into old file and thus, diff + // command will show them as pruned. + for _, p := range prunedObjs { + name, err := getObjectName(p) + if err != nil { + klog.Warningf("pruning failed and object name could not be retrieved: %v", err) + continue + } + if err := differ.From.Print(name, p, printer); err != nil { + return err + } + } + } + if err != nil { return err } return differ.Run(o.Diff) } + +func getObjectName(obj runtime.Object) (string, error) { + gvk := obj.GetObjectKind().GroupVersionKind() + metadata, err := meta.Accessor(obj) + if err != nil { + return "", err + } + name := metadata.GetName() + ns := metadata.GetNamespace() + + group := "" + if gvk.Group != "" { + group = fmt.Sprintf("%v.", gvk.Group) + } + return group + fmt.Sprintf( + "%v.%v.%v.%v", + gvk.Version, + gvk.Kind, + ns, + name, + ), nil +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/diff/prune.go b/staging/src/k8s.io/kubectl/pkg/cmd/diff/prune.go new file mode 100644 index 00000000000..64a6d4fee36 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/diff/prune.go @@ -0,0 +1,126 @@ +/* +Copyright 2021 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 diff + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/dynamic" + "k8s.io/kubectl/pkg/util/prune" +) + +type pruner struct { + mapper meta.RESTMapper + dynamicClient dynamic.Interface + + visitedUids sets.String + visitedNamespaces sets.String + labelSelector string + resources []prune.Resource +} + +func newPruner(dc dynamic.Interface, m meta.RESTMapper, r []prune.Resource) *pruner { + return &pruner{ + visitedUids: sets.NewString(), + visitedNamespaces: sets.NewString(), + dynamicClient: dc, + mapper: m, + resources: r, + } +} + +func (p *pruner) pruneAll() ([]runtime.Object, error) { + var allPruned []runtime.Object + namespacedRESTMappings, nonNamespacedRESTMappings, err := prune.GetRESTMappings(p.mapper, p.resources) + if err != nil { + return allPruned, fmt.Errorf("error retrieving RESTMappings to prune: %v", err) + } + + for n := range p.visitedNamespaces { + for _, m := range namespacedRESTMappings { + if pobjs, err := p.prune(n, m); err != nil { + return pobjs, fmt.Errorf("error pruning namespaced object %v: %v", m.GroupVersionKind, err) + } else { + allPruned = append(allPruned, pobjs...) + } + } + } + for _, m := range nonNamespacedRESTMappings { + if pobjs, err := p.prune(metav1.NamespaceNone, m); err != nil { + return allPruned, fmt.Errorf("error pruning nonNamespaced object %v: %v", m.GroupVersionKind, err) + } else { + allPruned = append(allPruned, pobjs...) + } + } + + return allPruned, nil +} + +func (p *pruner) prune(namespace string, mapping *meta.RESTMapping) ([]runtime.Object, error) { + objList, err := p.dynamicClient.Resource(mapping.Resource). + Namespace(namespace). + List(context.TODO(), metav1.ListOptions{ + LabelSelector: p.labelSelector, + }) + if err != nil { + return nil, err + } + + objs, err := meta.ExtractList(objList) + if err != nil { + return nil, err + } + + var pobjs []runtime.Object + for _, obj := range objs { + metadata, err := meta.Accessor(obj) + if err != nil { + return pobjs, err + } + annots := metadata.GetAnnotations() + if _, ok := annots[corev1.LastAppliedConfigAnnotation]; !ok { + continue + } + uid := metadata.GetUID() + if p.visitedUids.Has(string(uid)) { + continue + } + + pobjs = append(pobjs, obj) + } + return pobjs, nil +} + +// MarkVisited marks visited namespaces and uids +func (p *pruner) MarkVisited(info *resource.Info) { + if info.Namespaced() { + p.visitedNamespaces.Insert(info.Namespace) + } + + metadata, err := meta.Accessor(info.Object) + if err != nil { + return + } + p.visitedUids.Insert(string(metadata.GetUID())) +} diff --git a/staging/src/k8s.io/kubectl/pkg/util/prune/prune.go b/staging/src/k8s.io/kubectl/pkg/util/prune/prune.go new file mode 100644 index 00000000000..0d49153fe4c --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/util/prune/prune.go @@ -0,0 +1,105 @@ +/* +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 prune + +import ( + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type Resource struct { + group string + version string + kind string + namespaced bool +} + +func (pr Resource) String() string { + return fmt.Sprintf("%v/%v, Kind=%v, Namespaced=%v", pr.group, pr.version, pr.kind, pr.namespaced) +} + +func GetRESTMappings(mapper meta.RESTMapper, pruneResources []Resource) (namespaced, nonNamespaced []*meta.RESTMapping, err error) { + if len(pruneResources) == 0 { + // default allowlist + pruneResources = []Resource{ + {"", "v1", "ConfigMap", true}, + {"", "v1", "Endpoints", true}, + {"", "v1", "Namespace", false}, + {"", "v1", "PersistentVolumeClaim", true}, + {"", "v1", "PersistentVolume", false}, + {"", "v1", "Pod", true}, + {"", "v1", "ReplicationController", true}, + {"", "v1", "Secret", true}, + {"", "v1", "Service", true}, + {"batch", "v1", "Job", true}, + {"batch", "v1", "CronJob", true}, + {"networking.k8s.io", "v1", "Ingress", true}, + {"apps", "v1", "DaemonSet", true}, + {"apps", "v1", "Deployment", true}, + {"apps", "v1", "ReplicaSet", true}, + {"apps", "v1", "StatefulSet", true}, + } + } + + for _, resource := range pruneResources { + addedMapping, err := mapper.RESTMapping(schema.GroupKind{Group: resource.group, Kind: resource.kind}, resource.version) + if err != nil { + return nil, nil, fmt.Errorf("invalid resource %v: %v", resource, err) + } + if resource.namespaced { + namespaced = append(namespaced, addedMapping) + } else { + nonNamespaced = append(nonNamespaced, addedMapping) + } + } + + return namespaced, nonNamespaced, nil +} + +func ParseResources(mapper meta.RESTMapper, gvks []string) ([]Resource, error) { + pruneResources := []Resource{} + for _, groupVersionKind := range gvks { + gvk := strings.Split(groupVersionKind, "/") + if len(gvk) != 3 { + return nil, fmt.Errorf("invalid GroupVersionKind format: %v, please follow ", groupVersionKind) + } + + if gvk[0] == "core" { + gvk[0] = "" + } + mapping, err := mapper.RESTMapping(schema.GroupKind{Group: gvk[0], Kind: gvk[2]}, gvk[1]) + if err != nil { + return pruneResources, err + } + var namespaced bool + namespaceScope := mapping.Scope.Name() + switch namespaceScope { + case meta.RESTScopeNameNamespace: + namespaced = true + case meta.RESTScopeNameRoot: + namespaced = false + default: + return pruneResources, fmt.Errorf("Unknown namespace scope: %q", namespaceScope) + } + + pruneResources = append(pruneResources, Resource{gvk[0], gvk[1], gvk[2], namespaced}) + } + return pruneResources, nil +} diff --git a/staging/src/k8s.io/kubectl/pkg/util/prune/prune_test.go b/staging/src/k8s.io/kubectl/pkg/util/prune/prune_test.go new file mode 100644 index 00000000000..cbc389b4af0 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/util/prune/prune_test.go @@ -0,0 +1,125 @@ +/* +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 prune + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type testRESTMapper struct { + meta.RESTMapper + scope meta.RESTScope +} + +func (m *testRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { + return &meta.RESTMapping{ + Resource: schema.GroupVersionResource{ + Group: gk.Group, + Version: "", + Resource: "", + }, + GroupVersionKind: schema.GroupVersionKind{ + Group: gk.Group, + Version: "", + Kind: gk.Kind, + }, + Scope: m.scope, + }, nil +} + +func TestGetRESTMappings(t *testing.T) { + tests := []struct { + mapper *testRESTMapper + pr []Resource + expectedns int + expectednns int + expectederr error + }{ + { + mapper: &testRESTMapper{}, + pr: []Resource{}, + expectedns: 14, + expectednns: 2, + expectederr: nil, + }, + } + + for _, tc := range tests { + actualns, actualnns, actualerr := GetRESTMappings(tc.mapper, tc.pr) + if tc.expectederr != nil { + assert.NotEmptyf(t, actualerr, "getRESTMappings error expected but not fired") + } + assert.Equal(t, len(actualns), tc.expectedns, "getRESTMappings failed expected number namespaced %d actual %d", tc.expectedns, len(actualns)) + assert.Equal(t, len(actualnns), tc.expectednns, "getRESTMappings failed expected number nonnamespaced %d actual %d", tc.expectednns, len(actualnns)) + } +} + +func TestParsePruneResources(t *testing.T) { + tests := []struct { + mapper *testRESTMapper + gvks []string + expected []Resource + err bool + }{ + { + mapper: &testRESTMapper{ + scope: meta.RESTScopeNamespace, + }, + gvks: nil, + expected: []Resource{}, + err: false, + }, + { + mapper: &testRESTMapper{ + scope: meta.RESTScopeNamespace, + }, + gvks: []string{"group/kind/version/test"}, + expected: []Resource{}, + err: true, + }, + { + mapper: &testRESTMapper{ + scope: meta.RESTScopeNamespace, + }, + gvks: []string{"group/kind/version"}, + expected: []Resource{{group: "group", version: "kind", kind: "version", namespaced: true}}, + err: false, + }, + { + mapper: &testRESTMapper{ + scope: meta.RESTScopeRoot, + }, + gvks: []string{"group/kind/version"}, + expected: []Resource{{group: "group", version: "kind", kind: "version", namespaced: false}}, + err: false, + }, + } + + for _, tc := range tests { + actual, err := ParseResources(tc.mapper, tc.gvks) + if tc.err { + assert.NotEmptyf(t, err, "parsePruneResources error expected but not fired") + } else { + assert.Equal(t, actual, tc.expected, "parsePruneResources failed expected %v actual %v", tc.expected, actual) + } + } +} diff --git a/test/cmd/diff.sh b/test/cmd/diff.sh index c1e75d683fe..7c5debba766 100755 --- a/test/cmd/diff.sh +++ b/test/cmd/diff.sh @@ -88,8 +88,29 @@ run_kubectl_diff_tests() { output_message=$(kubectl diff --server-side -f hack/testdata/pod-changed.yaml || test $? -eq 1) kube::test::if_has_string "${output_message}" 'k8s.gcr.io/pause:3.4' + ## kubectl diff --prune + kubectl create ns nsb + kubectl apply --namespace nsb -l prune-group=true -f hack/testdata/prune/a.yaml + kube::test::get_object_assert 'pods a -n nsb' "{{${id_field:?}}}" 'a' + # Make sure that kubectl diff does not return pod 'a' without prune flag + output_message=$(kubectl diff -l prune-group=true -f hack/testdata/prune/b.yaml || test $? -eq 1) + kube::test::if_has_not_string "${output_message}" "name: a" + # Make sure that for kubectl diff --prune: + # 1. the exit code for diff is 1 because it found a difference + # 2. the difference contains the pruned pod + output_message=$(kubectl diff --prune -l prune-group=true -f hack/testdata/prune/b.yaml || test $? -eq 1) + # pod 'a' should be in output, it is pruned + kube::test::if_has_string "${output_message}" 'name: a' + # apply b with namespace + kubectl apply --prune --namespace nsb -l prune-group=true -f hack/testdata/prune/b.yaml + # check right pod exists and wrong pod doesn't exist + kube::test::wait_object_assert 'pods -n nsb' "{{range.items}}{{${id_field:?}}}:{{end}}" 'b:' + # Make sure that diff --prune returns nothing (0 exit code) for 'b'. + kubectl diff --prune -l prune-group=true -f hack/testdata/prune/b.yaml + # Cleanup kubectl delete -f hack/testdata/pod.yaml + kubectl delete -f hack/testdata/prune/b.yaml set +o nounset set +o errexit diff --git a/vendor/modules.txt b/vendor/modules.txt index 966e85b0dfb..a6dcd0b2bf8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2153,6 +2153,7 @@ k8s.io/kubectl/pkg/util/openapi k8s.io/kubectl/pkg/util/openapi/testing k8s.io/kubectl/pkg/util/openapi/validation k8s.io/kubectl/pkg/util/podutils +k8s.io/kubectl/pkg/util/prune k8s.io/kubectl/pkg/util/qos k8s.io/kubectl/pkg/util/rbac k8s.io/kubectl/pkg/util/resource