diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/alpha.go b/staging/src/k8s.io/kubectl/pkg/cmd/alpha.go index bc32cee0eb0..a5ad73e23f3 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/alpha.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/alpha.go @@ -21,7 +21,6 @@ import ( "k8s.io/cli-runtime/pkg/genericiooptions" - cmdkuberc "k8s.io/kubectl/pkg/cmd/kuberc" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" @@ -35,11 +34,6 @@ func NewCmdAlpha(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.C Long: templates.LongDesc(i18n.T("These commands correspond to alpha features that are not enabled in Kubernetes clusters by default.")), } - // Add alpha commands - if !cmdutil.KubeRC.IsDisabled() { - cmd.AddCommand(cmdkuberc.NewCmdKubeRC(streams)) - } - // NewKubeletCommand() will hide the alpha command if it has no subcommands. Overriding // the help function ensures a reasonable message if someone types the hidden command anyway. if !cmd.HasAvailableSubCommands() { diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go b/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go index 290a65d4f60..16c417b01fe 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go @@ -55,6 +55,7 @@ import ( "k8s.io/kubectl/pkg/cmd/explain" "k8s.io/kubectl/pkg/cmd/expose" "k8s.io/kubectl/pkg/cmd/get" + kuberccmd "k8s.io/kubectl/pkg/cmd/kuberc" "k8s.io/kubectl/pkg/cmd/kustomize" "k8s.io/kubectl/pkg/cmd/label" "k8s.io/kubectl/pkg/cmd/logs" @@ -354,6 +355,9 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command { cmds.AddCommand(apiresources.NewCmdAPIVersions(f, o.IOStreams)) cmds.AddCommand(apiresources.NewCmdAPIResources(f, o.IOStreams)) cmds.AddCommand(options.NewCmdOptions(o.IOStreams.Out)) + if !cmdutil.KubeRC.IsDisabled() { + cmds.AddCommand(kuberccmd.NewCmdKubeRC(o.IOStreams)) + } // Stop warning about normalization of flags. That makes it possible to // add the klog flags later. diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/kuberc.go b/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/kuberc.go index d134fbd4c2e..212af0223ab 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/kuberc.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/kuberc.go @@ -32,13 +32,13 @@ var ( kubercExample = templates.Examples(i18n.T(` # View the current kuberc configuration - kubectl alpha kuberc view + kubectl kuberc view # Set a default value for a command flag - kubectl alpha kuberc set --section defaults --command get --option output=wide + kubectl kuberc set --section defaults --command get --option output=wide # Create an alias for a command - kubectl alpha kuberc set --section aliases --name getn --command get --prependarg nodes --option output=wide`)) + kubectl kuberc set --section aliases --name getn --command get --prependarg nodes --option output=wide`)) ) // NewCmdKubeRC creates a command object for the "kuberc" action, and adds all child commands to it. diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/set.go b/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/set.go index 8603bde08f6..26219336682 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/set.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/set.go @@ -52,19 +52,19 @@ var ( setExample = templates.Examples(i18n.T(` # Set default output format for 'get' command - kubectl alpha kuberc set --section defaults --command get --option output=wide + kubectl kuberc set --section defaults --command get --option output=wide # Set default output format for a subcommand - kubectl alpha kuberc set --section defaults --command "set env" --option output=yaml + kubectl kuberc set --section defaults --command "set env" --option output=yaml # Create an alias 'getn' for 'get' command with prepended 'nodes' resource - kubectl alpha kuberc set --section aliases --name getn --command get --prependarg nodes --option output=wide + kubectl kuberc set --section aliases --name getn --command get --prependarg nodes --option output=wide # Create an alias 'runx' for 'run' command with appended arguments - kubectl alpha kuberc set --section aliases --name runx --command run --option image=nginx --appendarg "--" --appendarg custom-arg1 + kubectl kuberc set --section aliases --name runx --command run --option image=nginx --appendarg "--" --appendarg custom-arg1 # Overwrite an existing default - kubectl alpha kuberc set --section defaults --command get --option output=json --overwrite`)) + kubectl kuberc set --section defaults --command get --option output=json --overwrite`)) ) // SetOptions contains the options for setting kuberc configuration diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/view.go b/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/view.go index b333b1aa89b..1b14cc57a4e 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/view.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/kuberc/view.go @@ -38,13 +38,13 @@ var ( viewExample = templates.Examples(i18n.T(` # View kuberc configuration in YAML format (default) - kubectl alpha kuberc view + kubectl kuberc view # View kuberc configuration in JSON format - kubectl alpha kuberc view --output json + kubectl kuberc view --output json # View a specific kuberc file - kubectl alpha kuberc view --kuberc /path/to/kuberc`)) + kubectl kuberc view --kuberc /path/to/kuberc`)) ) // ViewOptions contains the options for viewing kuberc configuration diff --git a/test/cmd/kuberc.sh b/test/cmd/kuberc.sh index da5a9a07549..f66e2dab4e8 100755 --- a/test/cmd/kuberc.sh +++ b/test/cmd/kuberc.sh @@ -23,7 +23,7 @@ run_kuberc_tests() { set -o errexit create_and_use_new_namespace - kube::log::status "Testing kubectl alpha kuberc set commands" + kube::log::status "Testing kubectl kuberc set commands" KUBERC_FILE="${TMPDIR:-/tmp}"/kuberc_file cat > "$KUBERC_FILE" << EOF @@ -31,21 +31,21 @@ apiVersion: kubectl.config.k8s.io/v1beta1 kind: Preference EOF - # Build up the kuberc file using kubectl alpha kuberc set commands - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=apply --option=server-side=true --option=dry-run=server --option=validate=strict - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=delete --option=interactive=true - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=get --option=namespace=test-kuberc-ns --option=output=json + # Build up the kuberc file using kubectl kuberc set commands + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=apply --option=server-side=true --option=dry-run=server --option=validate=strict + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=delete --option=interactive=true + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=get --option=namespace=test-kuberc-ns --option=output=json - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=crns --command="create namespace" --appendarg=test-kuberc-ns - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=getn --command=get --prependarg=namespace --option=output=wide - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=crole --command="create role" --option=verb=get,watch - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=getrole --command=get --option=output=json - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=runx --command=run --option=image=nginx --option=labels=app=test,env=test --option=env=DNS_DOMAIN=test --option=namespace=test-kuberc-ns --appendarg=test-pod-2 --appendarg=-- --appendarg=custom-arg1 --appendarg=custom-arg2 - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=setx --command="set image" --appendarg=pod/test-pod-2 --appendarg=test-pod-2=busybox + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=crns --command="create namespace" --appendarg=test-kuberc-ns + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=getn --command=get --prependarg=namespace --option=output=wide + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=crole --command="create role" --option=verb=get,watch + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=getrole --command=get --option=output=json + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=runx --command=run --option=image=nginx --option=labels=app=test,env=test --option=env=DNS_DOMAIN=test --option=namespace=test-kuberc-ns --appendarg=test-pod-2 --appendarg=-- --appendarg=custom-arg1 --appendarg=custom-arg2 + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=setx --command="set image" --appendarg=pod/test-pod-2 --appendarg=test-pod-2=busybox - kube::log::status "Testing kubectl alpha kuberc view commands" - # Test: kubectl alpha kuberc view - output_message=$(kubectl alpha kuberc view --kuberc="$KUBERC_FILE") + kube::log::status "Testing kubectl kuberc view commands" + # Test: kubectl kuberc view + output_message=$(kubectl kuberc view --kuberc="$KUBERC_FILE") kube::test::if_has_string "${output_message}" "apiVersion: kubectl.config.k8s.io/v1beta1" kube::test::if_has_string "${output_message}" "kind: Preference" kube::test::if_has_string "${output_message}" "command: apply" @@ -53,45 +53,45 @@ EOF kube::test::if_has_string "${output_message}" "server-side" kube::test::if_has_string "${output_message}" "interactive" - # Test: kubectl alpha kuberc view with json output - output_message=$(kubectl alpha kuberc view --kuberc="$KUBERC_FILE" -o json) + # Test: kubectl kuberc view with json output + output_message=$(kubectl kuberc view --kuberc="$KUBERC_FILE" -o json) kube::test::if_has_string "${output_message}" "\"apiVersion\": \"kubectl.config.k8s.io/v1beta1\"" kube::test::if_has_string "${output_message}" "\"kind\": \"Preference\"" # Test: Attempt to set existing default without --overwrite flag should fail - output_message=$(! kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=get --option=output=yaml 2>&1) + output_message=$(! kubectl kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=get --option=output=yaml 2>&1) kube::test::if_has_string "${output_message}" "defaults for command \"get\" already exist, use --overwrite to replace" # Test: Now set with --overwrite flag should succeed and merge options - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=get --option=output=yaml --overwrite - output_message=$(kubectl alpha kuberc view --kuberc="$KUBERC_FILE") + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=get --option=output=yaml --overwrite + output_message=$(kubectl kuberc view --kuberc="$KUBERC_FILE") kube::test::if_has_string "${output_message}" "default: yaml" # Should still have namespace option from before kube::test::if_has_string "${output_message}" "default: test-kuberc-ns" # Test: Attempt to set existing alias without --overwrite flag should fail - output_message=$(! kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=getn --command=get --prependarg=pods 2>&1) + output_message=$(! kubectl kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=getn --command=get --prependarg=pods 2>&1) kube::test::if_has_string "${output_message}" "alias \"getn\" already exists, use --overwrite to replace" # Test: Error cases - Missing required flags - output_message=$(! kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --command=get --option=output=wide 2>&1) + output_message=$(! kubectl kuberc set --kuberc="$KUBERC_FILE" --command=get --option=output=wide 2>&1) kube::test::if_has_string "${output_message}" "required flag(s) \"section\" not set" - output_message=$(! kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=defaults --option=output=wide 2>&1) + output_message=$(! kubectl kuberc set --kuberc="$KUBERC_FILE" --section=defaults --option=output=wide 2>&1) kube::test::if_has_string "${output_message}" "required flag(s) \"command\" not set" # Test: KUBERC=off with view command - output_message=$(! KUBERC=off kubectl alpha kuberc view 2>&1) + output_message=$(! KUBERC=off kubectl kuberc view 2>&1) kube::test::if_has_string "${output_message}" "KUBERC is disabled via KUBERC=off environment variable" # Test: KUBERC=off with set command - output_message=$(! KUBERC=off kubectl alpha kuberc set --section=defaults --command=get --option=output=wide 2>&1) + output_message=$(! KUBERC=off kubectl kuberc set --section=defaults --command=get --option=output=wide 2>&1) kube::test::if_has_string "${output_message}" "KUBERC is disabled via KUBERC=off environment variable" # Restore getn alias back to "namespace" for remaining tests - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=getn --command=get --prependarg=namespace --option=output=wide --overwrite + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=aliases --name=getn --command=get --prependarg=namespace --option=output=wide --overwrite # Restore get defaults back to namespace=test-kuberc-ns and output=json for remaining tests - kubectl alpha kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=get --option=namespace=test-kuberc-ns --option=output=json --overwrite + kubectl kuberc set --kuberc="$KUBERC_FILE" --section=defaults --command=get --option=namespace=test-kuberc-ns --option=output=json --overwrite kube::log::status "Testing kuberc aliases and defaults functionality" diff --git a/test/e2e/kubectl/kuberc.go b/test/e2e/kubectl/kuberc.go index 760ed2bd5d6..8e90da81297 100644 --- a/test/e2e/kubectl/kuberc.go +++ b/test/e2e/kubectl/kuberc.go @@ -28,6 +28,7 @@ import ( "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" + "k8s.io/kubectl/pkg/config/v1beta1" "sigs.k8s.io/yaml" @@ -162,4 +163,174 @@ var _ = SIGDescribe("kubectl kuberc", func() { gomega.Expect(output).NotTo(gomega.ContainSubstring("You are about to delete the following 1 resource(s)")) }) }) + ginkgo.Describe("commands", func() { + var tmpDir string + var kubercFile string + + ginkgo.BeforeEach(func() { + var err error + tmpDir, err = os.MkdirTemp("", "test-kuberc-cmd") + framework.ExpectNoError(err) + kubercFile = filepath.Join(tmpDir, "kuberc.yaml") + minimalKuberc := `apiVersion: kubectl.config.k8s.io/v1beta1 +kind: Preference +` + framework.ExpectNoError(os.WriteFile(kubercFile, []byte(minimalKuberc), os.FileMode(0644))) + }) + + ginkgo.AfterEach(func() { + if tmpDir != "" { + os.RemoveAll(tmpDir) //nolint:errcheck + } + }) + + ginkgo.It("view should display kuberc file", func(ctx context.Context) { + ginkgo.By("creating a kuberc file with defaults and aliases") + kubercContent := fmt.Sprintf(kuberc, imageutils.GetE2EImage(imageutils.BusyBox), ns, ns) + framework.ExpectNoError(os.WriteFile(kubercFile, []byte(kubercContent), os.FileMode(0755))) + + ginkgo.By("viewing the kuberc file and parsing as Preference") + output := e2ekubectl.RunKubectlOrDie(ns, "kuberc", "view", fmt.Sprintf("--kuberc=%s", kubercFile)) + var pref v1beta1.Preference + err := yaml.Unmarshal([]byte(output), &pref) + framework.ExpectNoError(err, "failed to unmarshal kuberc view output") + + ginkgo.By("verifying structure") + if pref.APIVersion != "kubectl.config.k8s.io/v1beta1" { + framework.Failf("expected apiVersion kubectl.config.k8s.io/v1beta1, got: %s", pref.APIVersion) + } + if pref.Kind != "Preference" { + framework.Failf("expected kind Preference, got: %s", pref.Kind) + } + if len(pref.Aliases) == 0 { + framework.Failf("expected aliases to be present") + } + if len(pref.Defaults) == 0 { + framework.Failf("expected defaults to be present") + } + }) + + ginkgo.It("set should create defaults and apply them to kubectl commands", func(ctx context.Context) { + ginkgo.By("setting default output format to yaml for get command") + e2ekubectl.RunKubectlOrDie(ns, "kuberc", "set", fmt.Sprintf("--kuberc=%s", kubercFile), "--section", "defaults", "--command", "get", "--option", "output=yaml") + + ginkgo.By("running kubectl get namespace with the kuberc file") + output := e2ekubectl.RunKubectlOrDie(ns, "get", "namespace", ns, fmt.Sprintf("--kuberc=%s", kubercFile)) + + ginkgo.By("verifying output is in YAML format due to kuberc default") + var namespace v1.Namespace + err := yaml.Unmarshal([]byte(output), &namespace) + framework.ExpectNoError(err, "expected output to be valid YAML") + if namespace.Name != ns { + framework.Failf("expected namespace name %s, got: %s", ns, namespace.Name) + } + + ginkgo.By("explicitly passing -ojson should override the kuberc default") + output = e2ekubectl.RunKubectlOrDie(ns, "get", "namespace", ns, fmt.Sprintf("--kuberc=%s", kubercFile), "-ojson") + + ginkgo.By("verifying output is in JSON format when explicitly requested") + var namespaceJSON v1.Namespace + err = json.Unmarshal([]byte(output), &namespaceJSON) + framework.ExpectNoError(err, "expected output to be valid JSON when explicitly passed -ojson") + if namespaceJSON.Name != ns { + framework.Failf("expected namespace name %s, got: %s", ns, namespaceJSON.Name) + } + }) + + ginkgo.It("set should create aliases with prependArgs and execute them", func(ctx context.Context) { + ginkgo.By("creating an alias 'getns' for 'get namespace' with yaml output") + e2ekubectl.RunKubectlOrDie(ns, "kuberc", "set", fmt.Sprintf("--kuberc=%s", kubercFile), + "--section", "aliases", + "--name", "getns", + "--command", "get", + "--prependarg", "namespace", + "--option", "output=yaml") + + ginkgo.By("using the alias to get namespace") + output := e2ekubectl.RunKubectlOrDie(ns, "getns", ns, fmt.Sprintf("--kuberc=%s", kubercFile)) + + ginkgo.By("verifying the alias worked and returned yaml output") + var namespace v1.Namespace + err := yaml.Unmarshal([]byte(output), &namespace) + framework.ExpectNoError(err, "expected alias output to be valid YAML") + if namespace.Name != ns { + framework.Failf("expected namespace name %s, got: %s", ns, namespace.Name) + } + }) + + ginkgo.It("set should create aliases with appendArgs and execute them", func(ctx context.Context) { + ginkgo.By("creating an alias with appendArgs for run command") + e2ekubectl.RunKubectlOrDie(ns, "kuberc", "set", fmt.Sprintf("--kuberc=%s", kubercFile), + "--section", "aliases", + "--name", "runtestpod", + "--command", "run", + "--option", "image="+imageutils.GetE2EImage(imageutils.BusyBox), + "--appendarg", "--", + "--appendarg", "sleep", + "--appendarg", "3600") + + ginkgo.By("using the alias to create a pod") + e2ekubectl.RunKubectlOrDie(ns, "runtestpod", "test-pod", fmt.Sprintf("--kuberc=%s", kubercFile), "-n", ns) + + ginkgo.By("verifying the pod was created with appended args") + output := e2ekubectl.RunKubectlOrDie(ns, "get", "pod", "test-pod", "-n", ns, "-oyaml") + var pod v1.Pod + err := yaml.Unmarshal([]byte(output), &pod) + framework.ExpectNoError(err) + + if len(pod.Spec.Containers) == 0 { + framework.Failf("expected pod to have at least one container") + } + if len(pod.Spec.Containers[0].Args) < 2 { + framework.Failf("expected pod to have appended args, got: %v", pod.Spec.Containers[0].Args) + } + foundSleep := false + for _, arg := range pod.Spec.Containers[0].Args { + if arg == "sleep" { + foundSleep = true + break + } + } + if !foundSleep { + framework.Failf("expected pod args to contain 'sleep' from appendArgs, got: %v", pod.Spec.Containers[0].Args) + } + }) + + ginkgo.It("set should require --overwrite to replace existing entries", func(ctx context.Context) { + ginkgo.By("creating initial default") + e2ekubectl.RunKubectlOrDie(ns, "kuberc", "set", fmt.Sprintf("--kuberc=%s", kubercFile), "--section", "defaults", "--command", "get", "--option", "output=yaml") + + ginkgo.By("attempting to overwrite without --overwrite flag should fail") + _, err := e2ekubectl.RunKubectl(ns, "kuberc", "set", fmt.Sprintf("--kuberc=%s", kubercFile), "--section", "defaults", "--command", "get", "--option", "output=json") + if err == nil { + framework.Failf("expected command to fail without --overwrite flag") + } + + ginkgo.By("overwriting with --overwrite flag should succeed") + e2ekubectl.RunKubectlOrDie(ns, "kuberc", "set", fmt.Sprintf("--kuberc=%s", kubercFile), "--section", "defaults", "--command", "get", "--option", "output=json", "--overwrite") + + ginkgo.By("verifying the updated default is applied") + output := e2ekubectl.RunKubectlOrDie(ns, "get", "namespace", ns, fmt.Sprintf("--kuberc=%s", kubercFile)) + var namespace v1.Namespace + err = json.Unmarshal([]byte(output), &namespace) + framework.ExpectNoError(err, "expected output to be JSON after overwrite") + if namespace.Name != ns { + framework.Failf("expected namespace name %s, got: %s", ns, namespace.Name) + } + }) + + ginkgo.It("set should validate inputs", func(ctx context.Context) { + ginkgo.By("attempting to create alias without --name should fail") + _, err := e2ekubectl.RunKubectl(ns, "kuberc", "set", fmt.Sprintf("--kuberc=%s", kubercFile), "--section", "aliases", "--command", "get") + if err == nil { + framework.Failf("expected command to fail when --name is missing for aliases") + } + + ginkgo.By("attempting to use invalid section should fail") + _, err = e2ekubectl.RunKubectl(ns, "kuberc", "set", fmt.Sprintf("--kuberc=%s", kubercFile), "--section", "invalid", "--command", "get") + if err == nil { + framework.Failf("expected command to fail with invalid section") + } + }) + }) })