diff --git a/README.md b/README.md index 60dc800d715ceab7cc0b8f768ab96b69407488ec..e21c9ac29cf9740886c67c49e0b044f34541e104 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ avoid an immediate `Killed: 9`. % kubecfg show -o yaml -f examples/guestbook.jsonnet # Create resources -% kubecfg update -f examples/guestbook.jsonnet +% kubecfg apply -f examples/guestbook.jsonnet # Modify configuration (downgrade gb-frontend image) % sed -i.bak '\,gcr.io/google-samples/gb-frontend,s/:v4/:v3/' examples/guestbook.jsonnet @@ -54,7 +54,7 @@ avoid an immediate `Killed: 9`. % kubecfg diff -f examples/guestbook.jsonnet # Update to new config -% kubecfg update -f examples/guestbook.jsonnet +% kubecfg apply -f examples/guestbook.jsonnet # Clean up after demo % kubecfg delete -f examples/guestbook.jsonnet diff --git a/cmd/apply.go b/cmd/apply.go new file mode 100644 index 0000000000000000000000000000000000000000..ab0065b3c9433f735eb8a81a32b0d8d9e5bf8413 --- /dev/null +++ b/cmd/apply.go @@ -0,0 +1,131 @@ +// Copyright 2017 The kubecfg 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 cmd + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/ksonnet/kubecfg/metadata" + "github.com/ksonnet/kubecfg/pkg/kubecfg" +) + +const ( + flagCreate = "create" + flagSkipGc = "skip-gc" + flagGcTag = "gc-tag" + flagDryRun = "dry-run" + + // AnnotationGcTag annotation that triggers + // garbage collection. Objects with value equal to + // command-line flag that are *not* in config will be deleted. + AnnotationGcTag = "kubecfg.ksonnet.io/garbage-collect-tag" + + // AnnotationGcStrategy controls gc logic. Current values: + // `auto` (default if absent) - do garbage collection + // `ignore` - never garbage collect this object + AnnotationGcStrategy = "kubecfg.ksonnet.io/garbage-collect-strategy" + + // GcStrategyAuto is the default automatic gc logic + GcStrategyAuto = "auto" + // GcStrategyIgnore means this object should be ignored by garbage collection + GcStrategyIgnore = "ignore" +) + +func init() { + RootCmd.AddCommand(applyCmd) + + addEnvCmdFlags(applyCmd) + applyCmd.PersistentFlags().Bool(flagCreate, true, "Create missing resources") + applyCmd.PersistentFlags().Bool(flagSkipGc, false, "Don't perform garbage collection, even with --"+flagGcTag) + applyCmd.PersistentFlags().String(flagGcTag, "", "Add this tag to updated objects, and garbage collect existing objects with this tag and not in config") + applyCmd.PersistentFlags().Bool(flagDryRun, false, "Perform only read-only operations") +} + +var applyCmd = &cobra.Command{ + Use: "apply [<env>|-f <file-or-dir>]", + Short: `Apply local configuration to remote cluster`, + RunE: func(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + var err error + + c := kubecfg.ApplyCmd{} + + c.Create, err = flags.GetBool(flagCreate) + if err != nil { + return err + } + + c.GcTag, err = flags.GetString(flagGcTag) + if err != nil { + return err + } + + c.SkipGc, err = flags.GetBool(flagSkipGc) + if err != nil { + return err + } + + c.DryRun, err = flags.GetBool(flagDryRun) + if err != nil { + return err + } + + c.ClientPool, c.Discovery, err = restClientPool(cmd) + if err != nil { + return err + } + + c.DefaultNamespace, _, err = clientConfig.Namespace() + if err != nil { + return err + } + + cwd, err := os.Getwd() + if err != nil { + return err + } + + objs, err := expandEnvCmdObjs(cmd, args) + if err != nil { + return err + } + + return c.Run(objs, metadata.AbsPath(cwd)) + }, + Long: `Update (or optionally create) Kubernetes resources on the cluster using the +local configuration. Use the '--create' flag to control whether we create them +if they do not exist (default: true). + +ksonnet applications are accepted, as well as normal JSON, YAML, and Jsonnet +files.`, + Example: ` # Create or update all resources described in a ksonnet application, and + # running in the 'dev' environment. Can be used in any subdirectory of the + # application. + ksonnet apply dev + + # Create or update resources described in a YAML file. Automatically picks up + # the cluster's location from '$KUBECONFIG'. + ksonnet appy -f ./pod.yaml + + # Update resources described in a YAML file, and running in cluster referred + # to by './kubeconfig'. + ksonnet apply --kubeconfig=./kubeconfig -f ./pod.yaml + + # Display set of actions we will execute when we run 'apply'. + ksonnet apply dev --dry-run`, +} diff --git a/cmd/root.go b/cmd/root.go index 7f24777cd2cf1210d8f48bacf7e8104addf5d6f1..ca20065bb3a48590dda2bb2e9c0b8f83a72778cc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -52,7 +52,7 @@ const ( flagResolver = "resolve-images" flagResolvFail = "resolve-images-error" - // For use in the commands (e.g., diff, update, delete) that require either an + // For use in the commands (e.g., diff, apply, delete) that require either an // environment or the -f flag. flagFile = "file" flagFileShort = "f" @@ -245,13 +245,13 @@ func restClientPool(cmd *cobra.Command) (dynamic.ClientPool, discovery.Discovery } // addEnvCmdFlags adds the flags that are common to the family of commands -// whose form is `[<env>|-f <file-name>]`, e.g., `update` and `delete`. +// whose form is `[<env>|-f <file-name>]`, e.g., `apply` and `delete`. func addEnvCmdFlags(cmd *cobra.Command) { cmd.PersistentFlags().StringArrayP(flagFile, flagFileShort, nil, "Filename or directory that contains the configuration to apply (accepts YAML, JSON, and Jsonnet)") } // parseEnvCmd parses the family of commands that come in the form `[<env>|-f -// <file-name>]`, e.g., `update` and `delete`. +// <file-name>]`, e.g., `apply` and `delete`. func parseEnvCmd(cmd *cobra.Command, args []string) (*string, []string, error) { flags := cmd.Flags() @@ -269,7 +269,7 @@ func parseEnvCmd(cmd *cobra.Command, args []string) (*string, []string, error) { } // expandEnvCmdObjs finds and expands templates for the family of commands of -// the form `[<env>|-f <file-name>]`, e.g., `update` and `delete`. That is, if +// the form `[<env>|-f <file-name>]`, e.g., `apply` and `delete`. That is, if // the user passes a list of files, we will expand all templates in those files, // while if a user passes an environment name, we will expand all component // files using that environment. diff --git a/cmd/update.go b/cmd/update.go index 8f0e72120a7b8eb8b9748de24d03256a11fb1385..ffe4514cfa18f640bb4b41611ee6f87c509a3039 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -24,28 +24,6 @@ import ( "github.com/ksonnet/kubecfg/pkg/kubecfg" ) -const ( - flagCreate = "create" - flagSkipGc = "skip-gc" - flagGcTag = "gc-tag" - flagDryRun = "dry-run" - - // AnnotationGcTag annotation that triggers - // garbage collection. Objects with value equal to - // command-line flag that are *not* in config will be deleted. - AnnotationGcTag = "kubecfg.ksonnet.io/garbage-collect-tag" - - // AnnotationGcStrategy controls gc logic. Current values: - // `auto` (default if absent) - do garbage collection - // `ignore` - never garbage collect this object - AnnotationGcStrategy = "kubecfg.ksonnet.io/garbage-collect-strategy" - - // GcStrategyAuto is the default automatic gc logic - GcStrategyAuto = "auto" - // GcStrategyIgnore means this object should be ignored by garbage collection - GcStrategyIgnore = "ignore" -) - func init() { RootCmd.AddCommand(updateCmd) @@ -57,14 +35,16 @@ func init() { } var updateCmd = &cobra.Command{ - Use: "update [<env>|-f <file-or-dir>]", - Short: `Update (or optionally create) Kubernetes resources on the cluster using the + Deprecated: "NOTE: Command 'update' is deprecated, use 'apply' instead", + Hidden: true, + Use: "update [<env>|-f <file-or-dir>]", + Short: `[DEPRECATED] Update (or optionally create) Kubernetes resources on the cluster using the local configuration. Accepts JSON, YAML, or Jsonnet.`, RunE: func(cmd *cobra.Command, args []string) error { flags := cmd.Flags() var err error - c := kubecfg.UpdateCmd{} + c := kubecfg.ApplyCmd{} c.Create, err = flags.GetBool(flagCreate) if err != nil { @@ -108,7 +88,9 @@ local configuration. Accepts JSON, YAML, or Jsonnet.`, return c.Run(objs, metadata.AbsPath(cwd)) }, - Long: `Update (or optionally create) Kubernetes resources on the cluster using the + Long: `NOTE: Command 'update' is deprecated, use 'apply' instead. + +Update (or optionally create) Kubernetes resources on the cluster using the local configuration. Use the '--create' flag to control whether we create them if they do not exist (default: true). diff --git a/examples/guestbook.jsonnet b/examples/guestbook.jsonnet index eef3f277478208d4c574be7b85104baad4f7226e..bc86ca5383f7e34309639fc36513dc871db41d91 100644 --- a/examples/guestbook.jsonnet +++ b/examples/guestbook.jsonnet @@ -23,7 +23,7 @@ // Expects to be run with ../lib in the jsonnet search path: // ``` // export KUBECFG_JPATH=$PWD/../lib -// kubecfg update guestbook.jsonnet +// kubecfg apply guestbook.jsonnet // # poke at $(minikube service --url frontend), etc // kubecfg delete guestbook.jsonnet // ``` diff --git a/integration/apply_test.go b/integration/apply_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0a101b9f1eba70aeb2c555405d22406039407543 --- /dev/null +++ b/integration/apply_test.go @@ -0,0 +1,124 @@ +// +build integration + +package integration + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/pkg/api/v1" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func cmData(cm *v1.ConfigMap) map[string]string { + return cm.Data +} + +var _ = Describe("apply", func() { + var c corev1.CoreV1Interface + var ns string + const cmName = "testcm" + + BeforeEach(func() { + c = corev1.NewForConfigOrDie(clusterConfigOrDie()) + ns = createNsOrDie(c, "apply") + }) + AfterEach(func() { + deleteNsOrDie(c, ns) + }) + + Describe("A simple apply", func() { + var cm *v1.ConfigMap + BeforeEach(func() { + cm = &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: cmName}, + Data: map[string]string{"foo": "bar"}, + } + }) + + JustBeforeEach(func() { + err := runKubecfgWith([]string{"apply", "-vv", "-n", ns}, []runtime.Object{cm}) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("With no existing state", func() { + It("should produce expected object", func() { + Expect(c.ConfigMaps(ns).Get("testcm", metav1.GetOptions{})). + To(WithTransform(cmData, HaveKeyWithValue("foo", "bar"))) + }) + }) + + Context("With existing object", func() { + BeforeEach(func() { + _, err := c.ConfigMaps(ns).Create(cm) + Expect(err).To(Not(HaveOccurred())) + }) + + It("should succeed", func() { + + Expect(c.ConfigMaps(ns).Get("testcm", metav1.GetOptions{})). + To(WithTransform(cmData, HaveKeyWithValue("foo", "bar"))) + }) + }) + + Context("With modified object", func() { + BeforeEach(func() { + otherCm := &v1.ConfigMap{ + ObjectMeta: cm.ObjectMeta, + Data: map[string]string{"foo": "not bar"}, + } + + _, err := c.ConfigMaps(ns).Create(otherCm) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should update the object", func() { + Expect(c.ConfigMaps(ns).Get("testcm", metav1.GetOptions{})). + To(WithTransform(cmData, HaveKeyWithValue("foo", "bar"))) + }) + }) + }) + + Describe("An apply with mixed namespaces", func() { + var ns2 string + BeforeEach(func() { + ns2 = createNsOrDie(c, "apply") + }) + AfterEach(func() { + deleteNsOrDie(c, ns2) + }) + + var objs []runtime.Object + BeforeEach(func() { + objs = []runtime.Object{ + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "nons"}, + }, + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: "ns1"}, + }, + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: ns2, Name: "ns2"}, + }, + } + }) + + JustBeforeEach(func() { + err := runKubecfgWith([]string{"apply", "-vv", "-n", ns}, objs) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should create objects in the correct namespaces", func() { + Expect(c.ConfigMaps(ns).Get("nons", metav1.GetOptions{})). + NotTo(BeNil()) + + Expect(c.ConfigMaps(ns).Get("ns1", metav1.GetOptions{})). + NotTo(BeNil()) + + Expect(c.ConfigMaps(ns2).Get("ns2", metav1.GetOptions{})). + NotTo(BeNil()) + }) + }) +}) diff --git a/integration/update_test.go b/integration/update_test.go index 0483e26ef8e1e43bbf6e94115179cd1403fd536f..bcd8c644403ae5cc73d523fcf662a760a38e0857 100644 --- a/integration/update_test.go +++ b/integration/update_test.go @@ -12,10 +12,6 @@ import ( . "github.com/onsi/gomega" ) -func cmData(cm *v1.ConfigMap) map[string]string { - return cm.Data -} - var _ = Describe("update", func() { var c corev1.CoreV1Interface var ns string diff --git a/pkg/kubecfg/update.go b/pkg/kubecfg/apply.go similarity index 97% rename from pkg/kubecfg/update.go rename to pkg/kubecfg/apply.go index 71c65df26b405fb67cb69da97d4e4fe1cd18d14b..b86e6b1520da1d9f186aa99e519f6cdf221364b9 100644 --- a/pkg/kubecfg/update.go +++ b/pkg/kubecfg/apply.go @@ -39,8 +39,8 @@ const ( GcStrategyIgnore = "ignore" ) -// UpdateCmd represents the update subcommand -type UpdateCmd struct { +// ApplyCmd represents the apply subcommand +type ApplyCmd struct { ClientPool dynamic.ClientPool Discovery discovery.DiscoveryInterface DefaultNamespace string @@ -51,7 +51,7 @@ type UpdateCmd struct { DryRun bool } -func (c UpdateCmd) Run(apiObjects []*unstructured.Unstructured, wd metadata.AbsPath) error { +func (c ApplyCmd) Run(apiObjects []*unstructured.Unstructured, wd metadata.AbsPath) error { dryRunText := "" if c.DryRun { dryRunText = " (dry-run)" diff --git a/pkg/kubecfg/update_test.go b/pkg/kubecfg/apply_test.go similarity index 100% rename from pkg/kubecfg/update_test.go rename to pkg/kubecfg/apply_test.go