diff --git a/internal/pkg/apply/apply.go b/internal/pkg/apply/apply.go index c8f2734..8746646 100644 --- a/internal/pkg/apply/apply.go +++ b/internal/pkg/apply/apply.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/cli-experimental/internal/pkg/client" "sigs.k8s.io/cli-experimental/internal/pkg/clik8s" + "sigs.k8s.io/cli-experimental/internal/pkg/constants" "sigs.k8s.io/kustomize/pkg/inventory" ) @@ -67,7 +68,12 @@ func (a *Apply) Do() (Result, error) { fmt.Fprintf(a.Out, "failed to update inventory object %v\n", err) } } - + if presence, ok := annotation[constants.Presence]; ok { + if presence == constants.EnsureNoExist { + // not applying the resource + continue + } + } err := a.DynamicClient.Apply(context.Background(), u) if err != nil { fmt.Fprintf(a.Out, "failed to apply the object: %s/%s: %v\n", u.GetKind(), u.GetName(), err) diff --git a/internal/pkg/apply/apply_test.go b/internal/pkg/apply/apply_test.go index 36b07ea..23134a3 100644 --- a/internal/pkg/apply/apply_test.go +++ b/internal/pkg/apply/apply_test.go @@ -15,10 +15,16 @@ package apply_test import ( "bytes" + "context" + "io/ioutil" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" "gopkg.in/src-d/go-git.v4/plumbing/object" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-experimental/internal/pkg/apply" "sigs.k8s.io/cli-experimental/internal/pkg/clik8s" "sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest" @@ -59,3 +65,137 @@ func TestApply(t *testing.T) { assert.NoError(t, err) assert.Equal(t, apply.Result{updatedObjects}, r) } + +func InitializeKustomizationWithPresence() ([]string, func(), error) { + f1, err := ioutil.TempDir("/tmp", "TestApply") + if err != nil { + return nil, nil, err + } + err = ioutil.WriteFile(filepath.Join(f1, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +configMapGenerator: +- name: test-map + +inventory: + type: ConfigMap + configMap: + name: inventory + namespace: default + +resources: +- not-apply-service.yaml + +namespace: default +`), 0644) + if err != nil { + return nil, nil, err + } + + err = ioutil.WriteFile(filepath.Join(f1, "not-apply-service.yaml"), []byte(` +apiVersion: v1 +kind: Service +metadata: + name: my-service + annotations: + kubectl.kubernetes.io/presence: EnsureDoesNotExist +spec: + selector: + app: MyApp + ports: + - protocol: TCP + port: 80 + targetPort: 9376 +`), 0644) + if err != nil { + return nil, nil, err + } + + f2, err := ioutil.TempDir("/tmp", "TestApply") + if err != nil { + return nil, nil, err + } + err = ioutil.WriteFile(filepath.Join(f2, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +configMapGenerator: +- name: test-map + literals: + - foo=bar + +inventory: + type: ConfigMap + configMap: + name: inventory + namespace: default + +resources: +- not-apply-service.yaml + +namespace: default +`), 0644) + if err != nil { + return nil, nil, err + } + + err = ioutil.WriteFile(filepath.Join(f2, "not-apply-service.yaml"), []byte(` +apiVersion: v1 +kind: Service +metadata: + name: my-service + annotations: + kubectl.kubernetes.io/presence: EnsureDoesNotExist +spec: + selector: + app: MyApp + ports: + - protocol: TCP + port: 80 + targetPort: 9376 +`), 0644) + if err != nil { + return nil, nil, err + } + + return []string{f1, f2}, func() { + os.RemoveAll(f1) + os.RemoveAll(f2) + }, nil +} + +func TestApplyWithPresenceAnnotation(t *testing.T) { + buf := new(bytes.Buffer) + kp := wiretest.InitializConfigProvider() + fs, cleanup, err := InitializeKustomizationWithPresence() + defer cleanup() + assert.NoError(t, err) + assert.Equal(t, len(fs), 2) + + objects, err := kp.GetConfig(fs[0]) + assert.NoError(t, err) + + a, done, err := wiretest.InitializeApply(objects, &object.Commit{}, buf) + defer done() + + serviceList := &unstructured.UnstructuredList{} + serviceList.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: "ServiceList", + Version: "v1", + }) + err = a.DynamicClient.List(context.Background(), serviceList, "default", nil) + defaultCount := len(serviceList.Items) + + assert.NoError(t, err) + r, err := a.Do() + assert.NoError(t, err) + assert.Equal(t, apply.Result{objects}, r) + err = a.DynamicClient.List(context.Background(), serviceList, "default", nil) + assert.Equal(t, len(serviceList.Items), defaultCount) + + updatedObjects, err := kp.GetConfig(fs[1]) + a.Resources = updatedObjects + assert.NoError(t, err) + r, err = a.Do() + assert.NoError(t, err) + assert.Equal(t, apply.Result{updatedObjects}, r) + err = a.DynamicClient.List(context.Background(), serviceList, "default", nil) + assert.Equal(t, len(serviceList.Items), defaultCount) +} diff --git a/internal/pkg/constants/constants.go b/internal/pkg/constants/constants.go new file mode 100644 index 0000000..ebf8bd1 --- /dev/null +++ b/internal/pkg/constants/constants.go @@ -0,0 +1,23 @@ +/* +Copyright 2019 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 constants + +const ( + Presence = "kubectl.kubernetes.io/presence" + EnsureExist = "EnsureExist" + EnsureNoExist = "EnsureDoesNotExist" +) \ No newline at end of file diff --git a/internal/pkg/delete/delete.go b/internal/pkg/delete/delete.go index fa9c9d5..90d4be2 100644 --- a/internal/pkg/delete/delete.go +++ b/internal/pkg/delete/delete.go @@ -18,10 +18,9 @@ import ( "fmt" "io" "os" + "sigs.k8s.io/cli-experimental/internal/pkg/util" "gopkg.in/src-d/go-git.v4/plumbing/object" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-experimental/internal/pkg/client" @@ -65,7 +64,7 @@ func (a *Delete) Do() (Result, error) { } } - err := a.deleteObject(ctx, u.GroupVersionKind(), u.GetNamespace(), u.GetName()) + _, err := util.DeleteObject(a.DynamicClient, ctx, u.GroupVersionKind(), u.GetNamespace(), u.GetName()) if err != nil { fmt.Fprint(os.Stderr, err) } @@ -94,7 +93,7 @@ func (a *Delete) handleInventroy(ctx context.Context, annotations map[string]str Version: id.Version, Kind: id.Kind, } - err = a.deleteObject(ctx, gvk, id.Namespace, id.Name) + _, err = util.DeleteObject(a.DynamicClient, ctx, gvk, id.Namespace, id.Name) if err != nil { fmt.Fprint(os.Stderr, err) } @@ -102,22 +101,6 @@ func (a *Delete) handleInventroy(ctx context.Context, annotations map[string]str return nil } -func (a *Delete) deleteObject(ctx context.Context, gvk schema.GroupVersionKind, ns, nm string) error { - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(gvk) - obj.SetNamespace(ns) - obj.SetName(nm) - - err := a.DynamicClient.Delete(ctx, obj, &metav1.DeleteOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return nil - } - return fmt.Errorf("failed to delete %s/%s: %v", gvk.Kind, nm, err) - } - return nil -} - // normalizeResourceOrdering move the inventory object to be the last resource // This is to make sure the inventory object is the last object to be deleted. func normalizeResourceOrdering(resources clik8s.ResourceConfigs) []*unstructured.Unstructured { diff --git a/internal/pkg/delete/delete_test.go b/internal/pkg/delete/delete_test.go index c5b35a3..72083a4 100644 --- a/internal/pkg/delete/delete_test.go +++ b/internal/pkg/delete/delete_test.go @@ -16,6 +16,9 @@ package delete_test import ( "bytes" "context" + "io/ioutil" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -78,3 +81,132 @@ func TestDelete(t *testing.T) { assert.NoError(t, err) assert.Equal(t, len(cmList.Items), 0) } + +func InitializeKustomizationWithPresence() ([]string, func(), error) { + f1, err := ioutil.TempDir("/tmp", "TestApply") + if err != nil { + return nil, nil, err + } + err = ioutil.WriteFile(filepath.Join(f1, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +configMapGenerator: +- name: test-map + +inventory: + type: ConfigMap + configMap: + name: inventory + namespace: default + +resources: +- not-delete-service.yaml + +namespace: default +`), 0644) + if err != nil { + return nil, nil, err + } + + err = ioutil.WriteFile(filepath.Join(f1, "not-delete-service.yaml"), []byte(` +apiVersion: v1 +kind: Service +metadata: + name: my-service + annotations: + kubectl.kubernetes.io/presence: EnsureExist +spec: + selector: + app: MyApp + ports: + - protocol: TCP + port: 80 + targetPort: 9376 +`), 0644) + if err != nil { + return nil, nil, err + } + + f2, err := ioutil.TempDir("/tmp", "TestApply") + if err != nil { + return nil, nil, err + } + err = ioutil.WriteFile(filepath.Join(f2, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +configMapGenerator: +- name: test-map + literals: + - foo=bar + +inventory: + type: ConfigMap + configMap: + name: inventory + namespace: default + +namespace: default +`), 0644) + if err != nil { + return nil, nil, err + } + + return []string{f1, f2}, func() { + os.RemoveAll(f1) + os.RemoveAll(f2) + }, nil +} + +func TestDeleteWithPresence(t *testing.T) { + buf := new(bytes.Buffer) + kp := wiretest.InitializConfigProvider() + fs, cleanup, err := InitializeKustomizationWithPresence() + defer cleanup() + assert.NoError(t, err) + assert.Equal(t, len(fs), 2) + + objects, err := kp.GetConfig(fs[0]) + assert.NoError(t, err) + a, donea, err := wiretest.InitializeApply(objects, &object.Commit{}, buf) + assert.NoError(t, err) + defer donea() + + serviceList := &unstructured.UnstructuredList{} + serviceList.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: "ServiceList", + Version: "v1", + }) + err = a.DynamicClient.List(context.Background(), serviceList, "default", nil) + defaultCount := len(serviceList.Items) + + _, err = a.Do() + assert.NoError(t, err) + updatedObjects, err := kp.GetConfig(fs[1]) + assert.NoError(t, err) + a.Resources = updatedObjects + _, err = a.Do() + assert.NoError(t, err) + + cmList := &unstructured.UnstructuredList{} + cmList.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: "ConfigMapList", + Version: "v1", + }) + err = a.DynamicClient.List(context.Background(), cmList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(cmList.Items), 3) + + d, doned, err := wiretest.InitializeDelete(updatedObjects, &object.Commit{}, buf) + defer doned() + assert.NoError(t, err) + d.DynamicClient = a.DynamicClient + _, err = d.Do() + assert.NoError(t, err) + + err = d.DynamicClient.List(context.Background(), cmList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(cmList.Items), 0) + + + err = a.DynamicClient.List(context.Background(), serviceList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(serviceList.Items), defaultCount + 1) +} diff --git a/internal/pkg/prune/prune.go b/internal/pkg/prune/prune.go index 78de134..5126933 100644 --- a/internal/pkg/prune/prune.go +++ b/internal/pkg/prune/prune.go @@ -18,12 +18,12 @@ import ( "fmt" "io" "os" + "sigs.k8s.io/cli-experimental/internal/pkg/util" "k8s.io/apimachinery/pkg/types" "gopkg.in/src-d/go-git.v4/plumbing/object" "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-experimental/internal/pkg/client" @@ -114,7 +114,7 @@ func (o *Prune) runPrune(ctx context.Context, obj *unstructured.Unstructured) ( Version: item.Version, Kind: item.Kind, } - u, err := o.deleteObject(ctx, gvk, item.Namespace, item.Name) + u, err := util.DeleteObject(o.DynamicClient, ctx, gvk, item.Namespace, item.Name) if err != nil { return nil, nil, err } @@ -127,19 +127,3 @@ func (o *Prune) runPrune(ctx context.Context, obj *unstructured.Unstructured) ( return obj, results, nil } -func (o *Prune) deleteObject(ctx context.Context, gvk schema.GroupVersionKind, - ns, nm string) (*unstructured.Unstructured, error) { - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(gvk) - obj.SetNamespace(ns) - obj.SetName(nm) - - err := o.DynamicClient.Delete(context.Background(), obj, &metav1.DeleteOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return nil, nil - } - return nil, fmt.Errorf("failed to delete %s/%s: %v", gvk.Kind, nm, err) - } - return obj, nil -} diff --git a/internal/pkg/util/client_util.go b/internal/pkg/util/client_util.go new file mode 100644 index 0000000..f6cca67 --- /dev/null +++ b/internal/pkg/util/client_util.go @@ -0,0 +1,51 @@ +package util + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/cli-experimental/internal/pkg/client" + "sigs.k8s.io/cli-experimental/internal/pkg/constants" +) + +// DeleteObject delete an object given a client and Group,Version,Kind,Name,Namespace of an object +func DeleteObject(c client.Client, ctx context.Context, gvk schema.GroupVersionKind, ns, nm string) ( + *unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + obj.SetNamespace(ns) + obj.SetName(nm) + + err := c.Get(ctx, types.NamespacedName{ + Namespace: ns, + Name: nm, + }, obj) + + if err != nil { + if errors.IsNotFound(err) { + return nil, nil + } + return nil, fmt.Errorf("failed to get %s/%s: %v", gvk.Kind, nm, err) + } + + annotations := obj.GetAnnotations() + if presence, ok := annotations[constants.Presence]; ok { + if presence == constants.EnsureExist { + // not delete the resource + return nil, nil + } + } + err = c.Delete(ctx, obj, &metav1.DeleteOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return nil, nil + } + return nil, fmt.Errorf("failed to delete %s/%s: %v", gvk.Kind, nm, err) + } + return obj, nil +}