From 349ab01372617b79b5f98cc6063dd3ad7baa5c12 Mon Sep 17 00:00:00 2001 From: David Xia Date: Wed, 19 Feb 2025 15:21:53 -0500 Subject: [PATCH] [refactor][kubectl-plugin] simplify cobra `Validate()` tests (#3040) We were duplicating logic in each command's `Validate()` test. 1. Abstract away K8s context detection logic with a new interface `KubeContexter`. 1. Consolidate tests for this logic in a separate test for `DefaultKubeContexter`. 1. Mock `KubeContexter` in `Validate()` tests with `MockKubeContexter` to simplify the `Validate()` tests. Signed-off-by: David Xia --- .../pkg/cmd/create/create_cluster.go | 8 +- .../pkg/cmd/create/create_cluster_test.go | 68 +--------- .../pkg/cmd/create/create_workergroup.go | 8 +- kubectl-plugin/pkg/cmd/delete/delete.go | 18 +-- kubectl-plugin/pkg/cmd/delete/delete_test.go | 63 +--------- kubectl-plugin/pkg/cmd/get/get_cluster.go | 18 +-- .../pkg/cmd/get/get_cluster_test.go | 70 ++--------- kubectl-plugin/pkg/cmd/get/get_nodes.go | 8 +- kubectl-plugin/pkg/cmd/get/get_nodes_test.go | 40 +----- kubectl-plugin/pkg/cmd/get/get_workergroup.go | 8 +- .../pkg/cmd/get/get_workergroup_test.go | 40 +----- kubectl-plugin/pkg/cmd/job/job_submit.go | 8 +- kubectl-plugin/pkg/cmd/job/job_submit_test.go | 57 ++------- kubectl-plugin/pkg/cmd/log/log.go | 26 ++-- kubectl-plugin/pkg/cmd/log/log_test.go | 118 ++++++------------ kubectl-plugin/pkg/cmd/session/session.go | 8 +- kubectl-plugin/pkg/cmd/version/version.go | 12 +- .../pkg/cmd/version/version_test.go | 55 ++------ kubectl-plugin/pkg/util/kubeconfig.go | 50 +++----- kubectl-plugin/pkg/util/kubeconfig_test.go | 64 ++++++++++ 20 files changed, 237 insertions(+), 510 deletions(-) create mode 100644 kubectl-plugin/pkg/util/kubeconfig_test.go diff --git a/kubectl-plugin/pkg/cmd/create/create_cluster.go b/kubectl-plugin/pkg/cmd/create/create_cluster.go index 9b97d2691b1..0e8c5141bc3 100644 --- a/kubectl-plugin/pkg/cmd/create/create_cluster.go +++ b/kubectl-plugin/pkg/cmd/create/create_cluster.go @@ -19,6 +19,7 @@ import ( type CreateClusterOptions struct { configFlags *genericclioptions.ConfigFlags ioStreams *genericclioptions.IOStreams + kubeContexter util.KubeContexter clusterName string rayVersion string image string @@ -48,8 +49,9 @@ var ( func NewCreateClusterOptions(streams genericclioptions.IOStreams) *CreateClusterOptions { return &CreateClusterOptions{ - configFlags: genericclioptions.NewConfigFlags(true), - ioStreams: &streams, + configFlags: genericclioptions.NewConfigFlags(true), + ioStreams: &streams, + kubeContexter: &util.DefaultKubeContexter{}, } } @@ -112,7 +114,7 @@ func (options *CreateClusterOptions) Validate() error { if err != nil { return fmt.Errorf("error retrieving raw config: %w", err) } - if !util.HasKubectlContext(config, options.configFlags) { + if !options.kubeContexter.HasContext(config, options.configFlags) { return fmt.Errorf("no context is currently set, use %q or %q to select a new one", "--context", "kubectl config use-context ") } diff --git a/kubectl-plugin/pkg/cmd/create/create_cluster_test.go b/kubectl-plugin/pkg/cmd/create/create_cluster_test.go index 7a47f680a73..3cbc6c2c7ee 100644 --- a/kubectl-plugin/pkg/cmd/create/create_cluster_test.go +++ b/kubectl-plugin/pkg/cmd/create/create_cluster_test.go @@ -23,80 +23,24 @@ func TestRayCreateClusterComplete(t *testing.T) { } func TestRayCreateClusterValidate(t *testing.T) { - testStreams, _, _, _ := genericclioptions.NewTestIOStreams() - testNS, testContext, testBT, testImpersonate := "test-namespace", "test-context", "test-bearer-token", "test-person" - - kubeConfigWithCurrentContext, err := util.CreateTempKubeConfigFile(t, testContext) - require.NoError(t, err) - - kubeConfigWithoutCurrentContext, err := util.CreateTempKubeConfigFile(t, "") - require.NoError(t, err) - tests := []struct { name string opts *CreateClusterOptions expectError string }{ { - name: "Test validation when no context is set", + name: "should error when no K8s context is set", opts: &CreateClusterOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - }, - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(false), }, expectError: "no context is currently set, use \"--context\" or \"kubectl config use-context \" to select a new one", }, { - name: "no error when kubeconfig has current context and --context switch isn't set", - opts: &CreateClusterOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "no error when kubeconfig has no current context and --context switch is set", - opts: &CreateClusterOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - Context: &testContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "no error when kubeconfig has current context and --context switch is set", - opts: &CreateClusterOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - Context: &testContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "Successful submit job validation with RayJob", + name: "should not error when K8s context is set", opts: &CreateClusterOptions{ - configFlags: &genericclioptions.ConfigFlags{ - Namespace: &testNS, - Context: &testContext, - KubeConfig: &kubeConfigWithCurrentContext, - BearerToken: &testBT, - Impersonate: &testImpersonate, - ImpersonateGroup: &[]string{"fake-group"}, - }, - ioStreams: &testStreams, - clusterName: "fakeclustername", - rayVersion: "ray-version", - image: "ray-image", - headCPU: "5", - headGPU: "1", - headMemory: "5Gi", - workerReplicas: 3, - workerCPU: "4", - workerMemory: "5Gi", + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(true), }, }, } diff --git a/kubectl-plugin/pkg/cmd/create/create_workergroup.go b/kubectl-plugin/pkg/cmd/create/create_workergroup.go index e33c3505c42..41e12e39b11 100644 --- a/kubectl-plugin/pkg/cmd/create/create_workergroup.go +++ b/kubectl-plugin/pkg/cmd/create/create_workergroup.go @@ -23,6 +23,7 @@ import ( type CreateWorkerGroupOptions struct { configFlags *genericclioptions.ConfigFlags ioStreams *genericclioptions.IOStreams + kubeContexter util.KubeContexter clusterName string groupName string rayVersion string @@ -51,8 +52,9 @@ var ( func NewCreateWorkerGroupOptions(streams genericclioptions.IOStreams) *CreateWorkerGroupOptions { return &CreateWorkerGroupOptions{ - configFlags: genericclioptions.NewConfigFlags(true), - ioStreams: &streams, + configFlags: genericclioptions.NewConfigFlags(true), + ioStreams: &streams, + kubeContexter: &util.DefaultKubeContexter{}, } } @@ -117,7 +119,7 @@ func (options *CreateWorkerGroupOptions) Validate() error { if err != nil { return fmt.Errorf("Error retrieving raw config: %w", err) } - if !util.HasKubectlContext(config, options.configFlags) { + if !options.kubeContexter.HasContext(config, options.configFlags) { return fmt.Errorf("no context is currently set, use %q or %q to select a new one", "--context", "kubectl config use-context ") } diff --git a/kubectl-plugin/pkg/cmd/delete/delete.go b/kubectl-plugin/pkg/cmd/delete/delete.go index cf0bb95ea8e..099314e85b4 100644 --- a/kubectl-plugin/pkg/cmd/delete/delete.go +++ b/kubectl-plugin/pkg/cmd/delete/delete.go @@ -20,11 +20,12 @@ import ( ) type DeleteOptions struct { - configFlags *genericclioptions.ConfigFlags - ioStreams *genericiooptions.IOStreams - ResourceType util.ResourceType - ResourceName string - Namespace string + configFlags *genericclioptions.ConfigFlags + ioStreams *genericiooptions.IOStreams + kubeContexter util.KubeContexter + ResourceType util.ResourceType + ResourceName string + Namespace string } var deleteExample = templates.Examples(` @@ -44,8 +45,9 @@ var deleteExample = templates.Examples(` func NewDeleteOptions(streams genericiooptions.IOStreams) *DeleteOptions { configFlags := genericclioptions.NewConfigFlags(true) return &DeleteOptions{ - ioStreams: &streams, - configFlags: configFlags, + ioStreams: &streams, + configFlags: configFlags, + kubeContexter: &util.DefaultKubeContexter{}, } } @@ -118,7 +120,7 @@ func (options *DeleteOptions) Validate() error { if err != nil { return fmt.Errorf("Error retrieving raw config: %w", err) } - if !util.HasKubectlContext(config, options.configFlags) { + if !options.kubeContexter.HasContext(config, options.configFlags) { return fmt.Errorf("no context is currently set, use %q or %q to select a new one", "--context", "kubectl config use-context ") } return nil diff --git a/kubectl-plugin/pkg/cmd/delete/delete_test.go b/kubectl-plugin/pkg/cmd/delete/delete_test.go index b50826d57de..c2d36a84b58 100644 --- a/kubectl-plugin/pkg/cmd/delete/delete_test.go +++ b/kubectl-plugin/pkg/cmd/delete/delete_test.go @@ -113,75 +113,24 @@ func TestComplete(t *testing.T) { } func TestRayDeleteValidate(t *testing.T) { - testStreams, _, _, _ := genericclioptions.NewTestIOStreams() - - testNS, testContext, testBT, testImpersonate := "test-namespace", "test-context", "test-bearer-token", "test-person" - - kubeConfigWithCurrentContext, err := util.CreateTempKubeConfigFile(t, testContext) - require.NoError(t, err) - - kubeConfigWithoutCurrentContext, err := util.CreateTempKubeConfigFile(t, "") - require.NoError(t, err) - tests := []struct { name string opts *DeleteOptions expectError string }{ { - name: "Test validation when no context is set", + name: "should error when no K8s context is set", opts: &DeleteOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - }, - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(false), }, expectError: "no context is currently set, use \"--context\" or \"kubectl config use-context \" to select a new one", }, { - name: "no error when kubeconfig has current context and --context switch isn't set", - opts: &DeleteOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "no error when kubeconfig has no current context and --context switch is set", - opts: &DeleteOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - Context: &testContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "no error when kubeconfig has current context and --context switch is set", - opts: &DeleteOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - Context: &testContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "Successful submit job validation with RayJob", + name: "should not error when K8s context is set", opts: &DeleteOptions{ - configFlags: &genericclioptions.ConfigFlags{ - Namespace: &testNS, - Context: &testContext, - KubeConfig: &kubeConfigWithCurrentContext, - BearerToken: &testBT, - Impersonate: &testImpersonate, - ImpersonateGroup: &[]string{"fake-group"}, - }, - ioStreams: &testStreams, - ResourceType: util.RayJob, - ResourceName: "test-rayjob", - Namespace: testNS, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(true), }, }, } diff --git a/kubectl-plugin/pkg/cmd/get/get_cluster.go b/kubectl-plugin/pkg/cmd/get/get_cluster.go index 485a7c6dac3..5eb4c884d51 100644 --- a/kubectl-plugin/pkg/cmd/get/get_cluster.go +++ b/kubectl-plugin/pkg/cmd/get/get_cluster.go @@ -23,14 +23,16 @@ import ( type GetClusterOptions struct { configFlags *genericclioptions.ConfigFlags ioStreams *genericclioptions.IOStreams + kubeContexter util.KubeContexter cluster string - AllNamespaces bool + allNamespaces bool } func NewGetClusterOptions(streams genericclioptions.IOStreams) *GetClusterOptions { return &GetClusterOptions{ - configFlags: genericclioptions.NewConfigFlags(true), - ioStreams: &streams, + configFlags: genericclioptions.NewConfigFlags(true), + ioStreams: &streams, + kubeContexter: &util.DefaultKubeContexter{}, } } @@ -61,14 +63,14 @@ func NewGetClusterCommand(streams genericclioptions.IOStreams) *cobra.Command { return options.Run(cmd.Context(), k8sClient) }, } - cmd.Flags().BoolVarP(&options.AllNamespaces, "all-namespaces", "A", options.AllNamespaces, "If present, list the requested clusters across all namespaces. Namespace in current context is ignored even if specified with --namespace.") + cmd.Flags().BoolVarP(&options.allNamespaces, "all-namespaces", "A", options.allNamespaces, "If present, list the requested clusters across all namespaces. Namespace in current context is ignored even if specified with --namespace.") options.configFlags.AddFlags(cmd.Flags()) return cmd } func (options *GetClusterOptions) Complete(args []string) error { if *options.configFlags.Namespace == "" { - options.AllNamespaces = true + options.allNamespaces = true } if len(args) >= 1 { @@ -84,7 +86,7 @@ func (options *GetClusterOptions) Validate() error { if err != nil { return fmt.Errorf("Error retrieving raw config: %w", err) } - if !util.HasKubectlContext(config, options.configFlags) { + if !options.kubeContexter.HasContext(config, options.configFlags) { return fmt.Errorf("no context is currently set, use %q or %q to select a new one", "--context", "kubectl config use-context ") } return nil @@ -110,7 +112,7 @@ func getRayClusters(ctx context.Context, options *GetClusterOptions, k8sClient c } } - if options.AllNamespaces { + if options.allNamespaces { rayclusterList, err = k8sClient.RayClient().RayV1().RayClusters("").List(ctx, listopts) if err != nil { return nil, fmt.Errorf("unable to retrieve Ray clusters for all namespaces: %w", err) @@ -124,7 +126,7 @@ func getRayClusters(ctx context.Context, options *GetClusterOptions, k8sClient c if options.cluster != "" && len(rayclusterList.Items) == 0 { errMsg := fmt.Sprintf("Ray cluster %s not found", options.cluster) - if options.AllNamespaces { + if options.allNamespaces { errMsg += " in any namespace" } else { errMsg += fmt.Sprintf(" in namespace %s", *options.configFlags.Namespace) diff --git a/kubectl-plugin/pkg/cmd/get/get_cluster_test.go b/kubectl-plugin/pkg/cmd/get/get_cluster_test.go index 2c33fedbeb2..13af485d77c 100644 --- a/kubectl-plugin/pkg/cmd/get/get_cluster_test.go +++ b/kubectl-plugin/pkg/cmd/get/get_cluster_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/ray-project/kuberay/kubectl-plugin/pkg/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/resource" @@ -18,6 +17,7 @@ import ( "k8s.io/cli-runtime/pkg/printers" kubefake "k8s.io/client-go/kubernetes/fake" + "github.com/ray-project/kuberay/kubectl-plugin/pkg/util" "github.com/ray-project/kuberay/kubectl-plugin/pkg/util/client" rayv1 "github.com/ray-project/kuberay/ray-operator/apis/ray/v1" @@ -76,7 +76,7 @@ func TestRayClusterGetComplete(t *testing.T) { err := fakeClusterGetOptions.Complete(tc.args) require.NoError(t, err) - assert.Equal(t, tc.expectedAllNamespaces, fakeClusterGetOptions.AllNamespaces) + assert.Equal(t, tc.expectedAllNamespaces, fakeClusterGetOptions.allNamespaces) assert.Equal(t, tc.expectedCluster, fakeClusterGetOptions.cluster) }) } @@ -84,16 +84,6 @@ func TestRayClusterGetComplete(t *testing.T) { // Test the Validation() step of the command. func TestRayClusterGetValidate(t *testing.T) { - testStreams, _, _, _ := genericclioptions.NewTestIOStreams() - - testNS, testContext, testBT, testImpersonate := "test-namespace", "test-context", "test-bearer-token", "test-person" - - kubeConfigWithCurrentContext, err := util.CreateTempKubeConfigFile(t, testContext) - require.NoError(t, err) - - kubeConfigWithoutCurrentContext, err := util.CreateTempKubeConfigFile(t, "") - require.NoError(t, err) - tests := []struct { name string opts *GetClusterOptions @@ -101,63 +91,21 @@ func TestRayClusterGetValidate(t *testing.T) { expectError string }{ { - name: "Test validation when no context is set", + name: "should error when no K8s context is set", opts: &GetClusterOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - }, - AllNamespaces: false, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(false), cluster: "random_arg", - ioStreams: &testStreams, }, expectError: "no context is currently set, use \"--context\" or \"kubectl config use-context \" to select a new one", }, { - name: "no error when kubeconfig has current context and --context switch isn't set", + name: "should not error when K8s context is set", opts: &GetClusterOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "no error when kubeconfig has no current context and --context switch is set", - opts: &GetClusterOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - Context: &testContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "no error when kubeconfig has current context and --context switch is set", - opts: &GetClusterOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - Context: &testContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "Successful validation call", - opts: &GetClusterOptions{ - // Use fake config to bypass the config flag checks - configFlags: &genericclioptions.ConfigFlags{ - Namespace: &testNS, - Context: &testContext, - KubeConfig: &kubeConfigWithCurrentContext, - BearerToken: &testBT, - Impersonate: &testImpersonate, - ImpersonateGroup: &[]string{"fake-group"}, - }, - AllNamespaces: false, - cluster: "random_arg", - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(true), + cluster: "my-cluster", }, - expectError: "", }, } diff --git a/kubectl-plugin/pkg/cmd/get/get_nodes.go b/kubectl-plugin/pkg/cmd/get/get_nodes.go index 5c7a114faff..449be5745a8 100644 --- a/kubectl-plugin/pkg/cmd/get/get_nodes.go +++ b/kubectl-plugin/pkg/cmd/get/get_nodes.go @@ -23,6 +23,7 @@ import ( type GetNodesOptions struct { configFlags *genericclioptions.ConfigFlags ioStreams *genericclioptions.IOStreams + kubeContexter util.KubeContexter namespace string cluster string node string @@ -64,8 +65,9 @@ var getNodesExample = templates.Examples(` func NewGetNodesOptions(streams genericclioptions.IOStreams) *GetNodesOptions { return &GetNodesOptions{ - configFlags: genericclioptions.NewConfigFlags(true), - ioStreams: &streams, + configFlags: genericclioptions.NewConfigFlags(true), + ioStreams: &streams, + kubeContexter: &util.DefaultKubeContexter{}, } } @@ -126,7 +128,7 @@ func (options *GetNodesOptions) Validate() error { if err != nil { return fmt.Errorf("error retrieving raw config: %w", err) } - if !util.HasKubectlContext(config, options.configFlags) { + if !options.kubeContexter.HasContext(config, options.configFlags) { return fmt.Errorf("no context is currently set, use %q or %q to select a new one", "--context", "kubectl config use-context ") } return nil diff --git a/kubectl-plugin/pkg/cmd/get/get_nodes_test.go b/kubectl-plugin/pkg/cmd/get/get_nodes_test.go index 072867220c4..ee0d717837b 100644 --- a/kubectl-plugin/pkg/cmd/get/get_nodes_test.go +++ b/kubectl-plugin/pkg/cmd/get/get_nodes_test.go @@ -91,14 +91,6 @@ func TestRayNodesGetComplete(t *testing.T) { } func TestRayNodesGetValidate(t *testing.T) { - testContext := "test-context" - - kubeConfigWithCurrentContext, err := util.CreateTempKubeConfigFile(t, testContext) - require.NoError(t, err) - - kubeConfigWithoutCurrentContext, err := util.CreateTempKubeConfigFile(t, "") - require.NoError(t, err) - tests := []struct { name string opts *GetNodesOptions @@ -106,38 +98,18 @@ func TestRayNodesGetValidate(t *testing.T) { expectError string }{ { - name: "should error when no context is set", + name: "should error when no K8s context is set", opts: &GetNodesOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - }, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(false), }, expectError: "no context is currently set, use \"--context\" or \"kubectl config use-context \" to select a new one", }, { - name: "no error when kubeconfig has current context and --context switch isn't set", + name: "should not error when K8s context is set", opts: &GetNodesOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - }, - }, - }, - { - name: "no error when kubeconfig has no current context and --context switch is set", - opts: &GetNodesOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - Context: &testContext, - }, - }, - }, - { - name: "no error when kubeconfig has current context and --context switch is set", - opts: &GetNodesOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - Context: &testContext, - }, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(true), }, }, } diff --git a/kubectl-plugin/pkg/cmd/get/get_workergroup.go b/kubectl-plugin/pkg/cmd/get/get_workergroup.go index 7d7b83385c0..d8ab70e7e4e 100644 --- a/kubectl-plugin/pkg/cmd/get/get_workergroup.go +++ b/kubectl-plugin/pkg/cmd/get/get_workergroup.go @@ -23,6 +23,7 @@ import ( type GetWorkerGroupsOptions struct { configFlags *genericclioptions.ConfigFlags ioStreams *genericclioptions.IOStreams + kubeContexter util.KubeContexter namespace string cluster string workerGroup string @@ -69,8 +70,9 @@ var getWorkerGroupsExample = templates.Examples(` func NewGetWorkerGroupOptions(streams genericclioptions.IOStreams) *GetWorkerGroupsOptions { return &GetWorkerGroupsOptions{ - configFlags: genericclioptions.NewConfigFlags(true), - ioStreams: &streams, + configFlags: genericclioptions.NewConfigFlags(true), + ioStreams: &streams, + kubeContexter: &util.DefaultKubeContexter{}, } } @@ -131,7 +133,7 @@ func (options *GetWorkerGroupsOptions) Validate() error { if err != nil { return fmt.Errorf("error retrieving raw config: %w", err) } - if !util.HasKubectlContext(config, options.configFlags) { + if !options.kubeContexter.HasContext(config, options.configFlags) { return fmt.Errorf("no context is currently set, use %q or %q to select a new one", "--context", "kubectl config use-context ") } return nil diff --git a/kubectl-plugin/pkg/cmd/get/get_workergroup_test.go b/kubectl-plugin/pkg/cmd/get/get_workergroup_test.go index bb5a3634d8a..2fd33192167 100644 --- a/kubectl-plugin/pkg/cmd/get/get_workergroup_test.go +++ b/kubectl-plugin/pkg/cmd/get/get_workergroup_test.go @@ -92,14 +92,6 @@ func TestRayWorkerGroupGetComplete(t *testing.T) { } func TestRayWorkerGroupsGetValidate(t *testing.T) { - testContext := "test-context" - - kubeConfigWithCurrentContext, err := util.CreateTempKubeConfigFile(t, testContext) - require.NoError(t, err) - - kubeConfigWithoutCurrentContext, err := util.CreateTempKubeConfigFile(t, "") - require.NoError(t, err) - tests := []struct { name string opts *GetWorkerGroupsOptions @@ -107,38 +99,18 @@ func TestRayWorkerGroupsGetValidate(t *testing.T) { expectError string }{ { - name: "should error when no context is set", + name: "should error when no K8s context is set", opts: &GetWorkerGroupsOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - }, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(false), }, expectError: "no context is currently set, use \"--context\" or \"kubectl config use-context \" to select a new one", }, { - name: "no error when kubeconfig has current context and --context switch isn't set", + name: "should not error when K8s context is set", opts: &GetWorkerGroupsOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - }, - }, - }, - { - name: "no error when kubeconfig has no current context and --context switch is set", - opts: &GetWorkerGroupsOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - Context: &testContext, - }, - }, - }, - { - name: "no error when kubeconfig has current context and --context switch is set", - opts: &GetWorkerGroupsOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - Context: &testContext, - }, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(true), }, }, } diff --git a/kubectl-plugin/pkg/cmd/job/job_submit.go b/kubectl-plugin/pkg/cmd/job/job_submit.go index d34f48b66d5..e3a14e2cd6e 100644 --- a/kubectl-plugin/pkg/cmd/job/job_submit.go +++ b/kubectl-plugin/pkg/cmd/job/job_submit.go @@ -41,6 +41,7 @@ const ( type SubmitJobOptions struct { ioStreams *genericiooptions.IOStreams configFlags *genericclioptions.ConfigFlags + kubeContexter util.KubeContexter RayJob *rayv1.RayJob submissionID string entryPoint string @@ -105,8 +106,9 @@ var ( func NewJobSubmitOptions(streams genericiooptions.IOStreams) *SubmitJobOptions { return &SubmitJobOptions{ - ioStreams: &streams, - configFlags: genericclioptions.NewConfigFlags(true), + ioStreams: &streams, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: &util.DefaultKubeContexter{}, } } @@ -187,7 +189,7 @@ func (options *SubmitJobOptions) Validate() error { if err != nil { return fmt.Errorf("Error retrieving raw config: %w", err) } - if !util.HasKubectlContext(config, options.configFlags) { + if !options.kubeContexter.HasContext(config, options.configFlags) { return fmt.Errorf("no context is currently set, use %q or %q to select a new one", "--context", "kubectl config use-context ") } diff --git a/kubectl-plugin/pkg/cmd/job/job_submit_test.go b/kubectl-plugin/pkg/cmd/job/job_submit_test.go index fc737933e56..ed3a8a15180 100644 --- a/kubectl-plugin/pkg/cmd/job/job_submit_test.go +++ b/kubectl-plugin/pkg/cmd/job/job_submit_test.go @@ -29,8 +29,6 @@ func TestRayJobSubmitComplete(t *testing.T) { func TestRayJobSubmitValidate(t *testing.T) { testStreams, _, _, _ := genericclioptions.NewTestIOStreams() - testNS, testContext, testBT, testImpersonate := "test-namespace", "test-context", "test-bearer-token", "test-person" - fakeDir := t.TempDir() rayYaml := `apiVersion: ray.io/v1 @@ -48,66 +46,27 @@ spec: _, err = file.Write([]byte(rayYaml)) require.NoError(t, err) - kubeConfigWithCurrentContext, err := util.CreateTempKubeConfigFile(t, testContext) - require.NoError(t, err) - - kubeConfigWithoutCurrentContext, err := util.CreateTempKubeConfigFile(t, "") - require.NoError(t, err) - tests := []struct { name string opts *SubmitJobOptions expectError string }{ { - name: "Test validation when no context is set", + name: "should error when no K8s context is set", opts: &SubmitJobOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - }, - ioStreams: &testStreams, - fileName: rayJobYamlPath, - workingDir: "Fake/File/Path", + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(false), }, expectError: "no context is currently set, use \"--context\" or \"kubectl config use-context \" to select a new one", }, - { - name: "no error when kubeconfig has current context and --context switch isn't set", - opts: &SubmitJobOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - }, - ioStreams: &testStreams, - fileName: rayJobYamlPath, - workingDir: "Fake/File/Path", - }, - }, - { - name: "no error when kubeconfig has no current context and --context switch is set", - opts: &SubmitJobOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - Context: &testContext, - }, - ioStreams: &testStreams, - fileName: rayJobYamlPath, - workingDir: "Fake/File/Path", - }, - }, { name: "Successful submit job validation with RayJob", opts: &SubmitJobOptions{ - configFlags: &genericclioptions.ConfigFlags{ - Namespace: &testNS, - Context: &testContext, - KubeConfig: &kubeConfigWithCurrentContext, - BearerToken: &testBT, - Impersonate: &testImpersonate, - ImpersonateGroup: &[]string{"fake-group"}, - }, - ioStreams: &testStreams, - fileName: rayJobYamlPath, - workingDir: "Fake/File/Path", + configFlags: genericclioptions.NewConfigFlags(true), + ioStreams: &testStreams, + kubeContexter: util.NewMockKubeContexter(true), + fileName: rayJobYamlPath, + workingDir: "Fake/File/Path", }, }, } diff --git a/kubectl-plugin/pkg/cmd/log/log.go b/kubectl-plugin/pkg/cmd/log/log.go index e156b16ac52..0aa9c118fbf 100644 --- a/kubectl-plugin/pkg/cmd/log/log.go +++ b/kubectl-plugin/pkg/cmd/log/log.go @@ -72,13 +72,14 @@ func nodeTypeCompletion(_ *cobra.Command, _ []string, _ string) ([]string, cobra } type ClusterLogOptions struct { - configFlags *genericclioptions.ConfigFlags - ioStreams *genericclioptions.IOStreams - Executor RemoteExecutor - outputDir string - nodeType nodeTypeEnum - ResourceName string - ResourceType util.ResourceType + configFlags *genericclioptions.ConfigFlags + ioStreams *genericclioptions.IOStreams + kubeContexter util.KubeContexter + Executor RemoteExecutor + outputDir string + nodeType nodeTypeEnum + ResourceName string + ResourceType util.ResourceType } var ( @@ -109,10 +110,11 @@ var ( func NewClusterLogOptions(streams genericclioptions.IOStreams) *ClusterLogOptions { return &ClusterLogOptions{ - configFlags: genericclioptions.NewConfigFlags(true), - ioStreams: &streams, - Executor: &DefaultRemoteExecutor{}, - nodeType: allNodeType, + configFlags: genericclioptions.NewConfigFlags(true), + ioStreams: &streams, + kubeContexter: &util.DefaultKubeContexter{}, + Executor: &DefaultRemoteExecutor{}, + nodeType: allNodeType, } } @@ -191,7 +193,7 @@ func (options *ClusterLogOptions) Validate() error { if err != nil { return fmt.Errorf("Error retrieving raw config: %w", err) } - if !util.HasKubectlContext(config, options.configFlags) { + if !options.kubeContexter.HasContext(config, options.configFlags) { return fmt.Errorf("no context is currently set, use %q or %q to select a new one", "--context", "kubectl config use-context ") } diff --git a/kubectl-plugin/pkg/cmd/log/log_test.go b/kubectl-plugin/pkg/cmd/log/log_test.go index 482a5da9334..46ed4934a2f 100644 --- a/kubectl-plugin/pkg/cmd/log/log_test.go +++ b/kubectl-plugin/pkg/cmd/log/log_test.go @@ -188,26 +188,10 @@ func TestRayClusterLogComplete(t *testing.T) { func TestRayClusterLogValidate(t *testing.T) { testStreams, _, _, _ := genericclioptions.NewTestIOStreams() - testNS, testContext, testBT, testImpersonate := "test-namespace", "test-context", "test-bearer-token", "test-person" - - fakeDir := t.TempDir() - - kubeConfigWithCurrentContext, err := util.CreateTempKubeConfigFile(t, testContext) - require.NoError(t, err) - - kubeConfigWithoutCurrentContext, err := util.CreateTempKubeConfigFile(t, "") + tempDir := t.TempDir() + tempFile, err := os.CreateTemp(tempDir, "temp-file") require.NoError(t, err) - // Initialize the fake config flag with the fake kubeconfig and values - fakeConfigFlags := &genericclioptions.ConfigFlags{ - Namespace: &testNS, - Context: &testContext, - KubeConfig: &kubeConfigWithCurrentContext, - BearerToken: &testBT, - Impersonate: &testImpersonate, - ImpersonateGroup: &[]string{"fake-group"}, - } - tests := []struct { name string opts *ClusterLogOptions @@ -215,101 +199,71 @@ func TestRayClusterLogValidate(t *testing.T) { expectError string }{ { - name: "Test validation when no context is set", - opts: &ClusterLogOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - }, - outputDir: fakeDir, - ResourceName: "fake-cluster", - nodeType: headNodeType, - ioStreams: &testStreams, - }, - expectError: "no context is currently set, use \"--context\" or \"kubectl config use-context \" to select a new one", - }, - { - name: "no error when kubeconfig has current context and --context switch isn't set", + name: "should error when no K8s context is set", opts: &ClusterLogOptions{ // Use fake config to bypass the config flag checks - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - }, - outputDir: fakeDir, - ResourceName: "fake-cluster", - nodeType: workerNodeType, - ioStreams: &testStreams, - }, - }, - { - name: "no error when kubeconfig has no current context and --context switch is set", - opts: &ClusterLogOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - Context: &testContext, - }, - outputDir: fakeDir, - ResourceName: "fake-cluster", - nodeType: allNodeType, - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(false), }, + expectError: "no context is currently set, use \"--context\" or \"kubectl config use-context \" to select a new one", }, { name: "Test validation when node type is `random-string`", opts: &ClusterLogOptions{ - // Use fake config to bypass the config flag checks - configFlags: fakeConfigFlags, - outputDir: fakeDir, - ResourceName: "fake-cluster", - nodeType: "random-string", - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + outputDir: tempDir, + ResourceName: "fake-cluster", + nodeType: "random-string", + ioStreams: &testStreams, + kubeContexter: util.NewMockKubeContexter(true), }, expectError: "unknown node type `random-string`", }, { name: "Successful validation call", opts: &ClusterLogOptions{ - // Use fake config to bypass the config flag checks - configFlags: fakeConfigFlags, - outputDir: fakeDir, - ResourceName: "fake-cluster", - nodeType: headNodeType, - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + outputDir: tempDir, + ResourceName: "fake-cluster", + nodeType: headNodeType, + ioStreams: &testStreams, + kubeContexter: util.NewMockKubeContexter(true), }, expectError: "", }, { name: "Validate output directory when no out-dir is set.", opts: &ClusterLogOptions{ - // Use fake config to bypass the config flag checks - configFlags: fakeConfigFlags, - outputDir: "", - ResourceName: "fake-cluster", - nodeType: headNodeType, - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + outputDir: "", + ResourceName: "fake-cluster", + nodeType: headNodeType, + ioStreams: &testStreams, + kubeContexter: util.NewMockKubeContexter(true), }, expectError: "", }, { name: "Failed validation call with output directory not exist", opts: &ClusterLogOptions{ - // Use fake config to bypass the config flag checks - configFlags: fakeConfigFlags, - outputDir: "randomPath-here", - ResourceName: "fake-cluster", - nodeType: headNodeType, - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + outputDir: "randomPath-here", + ResourceName: "fake-cluster", + nodeType: headNodeType, + ioStreams: &testStreams, + kubeContexter: util.NewMockKubeContexter(true), }, expectError: "Directory does not exist. Failed with: stat randomPath-here: no such file or directory", }, { name: "Failed validation call with output directory is file", opts: &ClusterLogOptions{ - // Use fake config to bypass the config flag checks - configFlags: fakeConfigFlags, - outputDir: kubeConfigWithCurrentContext, - ResourceName: "fake-cluster", - nodeType: headNodeType, - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + outputDir: tempFile.Name(), + ResourceName: "fake-cluster", + nodeType: headNodeType, + ioStreams: &testStreams, + kubeContexter: util.NewMockKubeContexter(true), }, expectError: "Path is not a directory. Please input a directory and try again", }, diff --git a/kubectl-plugin/pkg/cmd/session/session.go b/kubectl-plugin/pkg/cmd/session/session.go index a0b48eacbaa..812f96dd5fb 100644 --- a/kubectl-plugin/pkg/cmd/session/session.go +++ b/kubectl-plugin/pkg/cmd/session/session.go @@ -26,6 +26,7 @@ type appPort struct { type SessionOptions struct { configFlags *genericclioptions.ConfigFlags ioStreams *genericiooptions.IOStreams + kubeContexter util.KubeContexter currentContext string ResourceType util.ResourceType ResourceName string @@ -73,8 +74,9 @@ var ( func NewSessionOptions(streams genericiooptions.IOStreams) *SessionOptions { configFlags := genericclioptions.NewConfigFlags(true) return &SessionOptions{ - ioStreams: &streams, - configFlags: configFlags, + ioStreams: &streams, + configFlags: configFlags, + kubeContexter: &util.DefaultKubeContexter{}, } } @@ -150,7 +152,7 @@ func (options *SessionOptions) Validate() error { if err != nil { return fmt.Errorf("Error retrieving raw config: %w", err) } - if !util.HasKubectlContext(config, options.configFlags) { + if !options.kubeContexter.HasContext(config, options.configFlags) { return fmt.Errorf("no context is currently set, use %q or %q to select a new one", "--context", "kubectl config use-context ") } options.currentContext = config.CurrentContext diff --git a/kubectl-plugin/pkg/cmd/version/version.go b/kubectl-plugin/pkg/cmd/version/version.go index d3a0979e8b7..47bab5d5ec5 100644 --- a/kubectl-plugin/pkg/cmd/version/version.go +++ b/kubectl-plugin/pkg/cmd/version/version.go @@ -16,14 +16,16 @@ import ( var Version = "development" type VersionOptions struct { - configFlags *genericclioptions.ConfigFlags - ioStreams *genericclioptions.IOStreams + configFlags *genericclioptions.ConfigFlags + ioStreams *genericclioptions.IOStreams + kubeContexter util.KubeContexter } func NewVersionOptions(streams genericclioptions.IOStreams) *VersionOptions { return &VersionOptions{ - configFlags: genericclioptions.NewConfigFlags(true), - ioStreams: &streams, + configFlags: genericclioptions.NewConfigFlags(true), + ioStreams: &streams, + kubeContexter: &util.DefaultKubeContexter{}, } } @@ -75,7 +77,7 @@ func (options *VersionOptions) checkContext() error { return fmt.Errorf("error retrieving raw config: %w", err) } - if !util.HasKubectlContext(config, options.configFlags) { + if !options.kubeContexter.HasContext(config, options.configFlags) { return fmt.Errorf("no context is currently set, use %q or %q to select a new one", "--context", "kubectl config use-context ") } return nil diff --git a/kubectl-plugin/pkg/cmd/version/version_test.go b/kubectl-plugin/pkg/cmd/version/version_test.go index 45bfb78104d..23b90516812 100644 --- a/kubectl-plugin/pkg/cmd/version/version_test.go +++ b/kubectl-plugin/pkg/cmd/version/version_test.go @@ -17,57 +17,24 @@ import ( ) func TestRayVersionCheckContext(t *testing.T) { - testStreams, _, _, _ := genericclioptions.NewTestIOStreams() - testContext := "test-context" - - kubeConfigWithCurrentContext, err := util.CreateTempKubeConfigFile(t, "my-fake-context") - require.NoError(t, err) - - kubeConfigWithoutCurrentContext, err := util.CreateTempKubeConfigFile(t, "") - require.NoError(t, err) - tests := []struct { name string opts *VersionOptions expectError string }{ { - name: "Test validation when no context is set", + name: "should error when no K8s context is set", opts: &VersionOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - }, - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(false), }, expectError: "no context is currently set, use \"--context\" or \"kubectl config use-context \" to select a new one", }, { - name: "no error when kubeconfig has current context and --context switch isn't set", - opts: &VersionOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "no error when kubeconfig has no current context and --context switch is set", + name: "should not error when K8s context is set", opts: &VersionOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithoutCurrentContext, - Context: &testContext, - }, - ioStreams: &testStreams, - }, - }, - { - name: "no error when kubeconfig has current context and --context switch is set", - opts: &VersionOptions{ - configFlags: &genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfigWithCurrentContext, - Context: &testContext, - }, - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(true), }, }, } @@ -112,15 +79,9 @@ func (c fakeClient) RayClient() rayclient.Interface { // Tests the Run() step of the command and checks the output. func TestRayVersionRun(t *testing.T) { - testContext := "test-context" - - testStreams, _, _, _ := genericclioptions.NewTestIOStreams() - fakeVersionOptions := &VersionOptions{ - configFlags: &genericclioptions.ConfigFlags{ - Context: &testContext, - }, - ioStreams: &testStreams, + configFlags: genericclioptions.NewConfigFlags(true), + kubeContexter: util.NewMockKubeContexter(true), } tests := []struct { diff --git a/kubectl-plugin/pkg/util/kubeconfig.go b/kubectl-plugin/pkg/util/kubeconfig.go index d64731ca7da..c35f8dfe048 100644 --- a/kubectl-plugin/pkg/util/kubeconfig.go +++ b/kubectl-plugin/pkg/util/kubeconfig.go @@ -1,47 +1,31 @@ package util import ( - "path/filepath" - "testing" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" ) +// KubeContexter is an interface that checks if the kubeconfig has a current context or if the --context switch is set. +type KubeContexter interface { + HasContext(config api.Config, configFlags *genericclioptions.ConfigFlags) bool +} + +type DefaultKubeContexter struct{} + // HasKubectlContext checks if the kubeconfig has a current context or if the --context switch is set. -func HasKubectlContext(config api.Config, configFlags *genericclioptions.ConfigFlags) bool { +func (d *DefaultKubeContexter) HasContext(config api.Config, configFlags *genericclioptions.ConfigFlags) bool { return config.CurrentContext != "" || (configFlags.Context != nil && *configFlags.Context != "") } -// CreateTempKubeConfigFile creates a temporary kubeconfig file with the given current context. -// This function should only be used in tests. -func CreateTempKubeConfigFile(t *testing.T, currentContext string) (string, error) { - tmpDir := t.TempDir() - - // Set up fake config for kubeconfig - config := &api.Config{ - Clusters: map[string]*api.Cluster{ - "test-cluster": { - Server: "https://fake-kubernetes-cluster.example.com", - InsecureSkipTLSVerify: true, // For testing purposes - }, - }, - Contexts: map[string]*api.Context{ - "my-fake-context": { - Cluster: "my-fake-cluster", - AuthInfo: "my-fake-user", - }, - }, - CurrentContext: currentContext, - AuthInfos: map[string]*api.AuthInfo{ - "my-fake-user": { - Token: "", // Empty for testing without authentication - }, - }, - } +// MockKubeContexter is a mock implementation of KubeContexter that should be used only in tests. +type MockKubeContexter struct { + returnValue bool +} - fakeFile := filepath.Join(tmpDir, ".kubeconfig") +func (m *MockKubeContexter) HasContext(_ api.Config, _ *genericclioptions.ConfigFlags) bool { + return m.returnValue +} - return fakeFile, clientcmd.WriteToFile(*config, fakeFile) +func NewMockKubeContexter(returnValue bool) *MockKubeContexter { + return &MockKubeContexter{returnValue: returnValue} } diff --git a/kubectl-plugin/pkg/util/kubeconfig_test.go b/kubectl-plugin/pkg/util/kubeconfig_test.go new file mode 100644 index 00000000000..87644cc33ee --- /dev/null +++ b/kubectl-plugin/pkg/util/kubeconfig_test.go @@ -0,0 +1,64 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/utils/ptr" +) + +func TestHasKubectlContext(t *testing.T) { + sut := &DefaultKubeContexter{} + + tests := []struct { + config api.Config + configFlags *genericclioptions.ConfigFlags + name string + expected bool + }{ + { + name: "should return false if there's no current context and no --context flag", + configFlags: &genericclioptions.ConfigFlags{}, + expected: false, + }, + { + name: "should return false if there's no current context and the --context flag is an empty string", + configFlags: &genericclioptions.ConfigFlags{ + Context: ptr.To(""), + }, + expected: false, + }, + { + name: "should return true if there's no current context but the --context flag is set", + configFlags: &genericclioptions.ConfigFlags{ + Context: ptr.To("my-context"), + }, + expected: true, + }, + { + name: "should return true if there's a current context but no --context flag", + config: api.Config{ + CurrentContext: "my-context", + }, + expected: true, + }, + { + name: "should return true if there's a current context and the --context flag is set", + config: api.Config{ + CurrentContext: "my-context", + }, + configFlags: &genericclioptions.ConfigFlags{ + Context: ptr.To("my-context"), + }, + expected: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, sut.HasContext(tc.config, tc.configFlags)) + }) + } +}