diff --git a/cli/cmds/cluster_create_flags.go b/cli/cmds/cluster_create_flags.go index 1cf71882..e9d6c946 100644 --- a/cli/cmds/cluster_create_flags.go +++ b/cli/cmds/cluster_create_flags.go @@ -37,6 +37,7 @@ func createFlags(cmd *cobra.Command, cfg *CreateConfig) { mustRegisterFlagCompletion(cmd, "mode", completeClusterMode) mustRegisterFlagCompletion(cmd, "persistence-type", completePersistenceMode) + mustRegisterFlagCompletion(cmd, "policy", completePolicyNames) if err := cmd.MarkFlagDirname("custom-certs"); err != nil { logrus.Fatal(err) diff --git a/cli/cmds/completion.go b/cli/cmds/completion.go index beefcb98..ff1a08e6 100644 --- a/cli/cmds/completion.go +++ b/cli/cmds/completion.go @@ -18,6 +18,7 @@ var completeClusterMode = cobra.FixedCompletions( []string{ string(v1beta1.SharedClusterMode), string(v1beta1.VirtualClusterMode), + string(v1beta1.HCPClusterMode), }, cobra.ShellCompDirectiveNoFileComp, ) @@ -73,6 +74,28 @@ func completeClusterNames(cmd *cobra.Command, args []string, toComplete string) return clusterNameCompletions(cmd.Context(), cl, namespace) } +// completePolicyNameArg is a cobra.CompletionFunc that completes a single policy name argument. +// It is the positional counterpart of completePolicyNames, which stays unguarded so that it can +// also be used as a flag completion, where args holds the positional arguments parsed so far. +func completePolicyNameArg(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // only the first positional argument is a policy name + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + return completePolicyNames(cmd, args, toComplete) +} + +// completePolicyNames is a cobra.CompletionFunc that completes with the k3k policy names. +func completePolicyNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cl, err := completionClient(cmd) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + return policyNameCompletions(cmd.Context(), cl) +} + // namespaceCompletions lists every namespace in the host cluster, excluding any // already provided to the command's "namespace" flag. func namespaceCompletions(cmd *cobra.Command, cl client.Client) ([]string, cobra.ShellCompDirective) { @@ -133,6 +156,21 @@ func clusterNameCompletions(ctx context.Context, cl client.Client, namespace str return names, cobra.ShellCompDirectiveNoFileComp } +// policyNameCompletions lists the k3k VirtualClusterPolicies by name. +func policyNameCompletions(ctx context.Context, cl client.Client) ([]string, cobra.ShellCompDirective) { + var policies v1beta1.VirtualClusterPolicyList + if err := cl.List(ctx, &policies); err != nil { + return nil, cobra.ShellCompDirectiveError + } + + names := make([]string, 0, len(policies.Items)) + for _, policy := range policies.Items { + names = append(names, policy.Name) + } + + return names, cobra.ShellCompDirectiveNoFileComp +} + // mustRegisterFlagCompletion registers a completion function for a flag and // aborts if the flag does not exist. This only fails on programmer error, so // there is no reason to bubble it up to the caller. diff --git a/cli/cmds/completion_test.go b/cli/cmds/completion_test.go index c6742ba3..24a7cd6b 100644 --- a/cli/cmds/completion_test.go +++ b/cli/cmds/completion_test.go @@ -36,6 +36,10 @@ func cluster(name, ns string) *v1beta1.Cluster { return &v1beta1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns}} } +func virtualClusterPolicy(name string) *v1beta1.VirtualClusterPolicy { + return &v1beta1.VirtualClusterPolicy{ObjectMeta: metav1.ObjectMeta{Name: name}} +} + func Test_completeNamespaces(t *testing.T) { fakeClient := fake.NewClientBuilder(). WithScheme(completionTestScheme(t)). @@ -127,3 +131,41 @@ func Test_completeClusterNames_filtersNamespace(t *testing.T) { // only the clusters in the requested namespace assert.ElementsMatch(t, []string{"k3k-bar/bar", "k3k-bar/bar-2"}, names) } + +func Test_completePolicyNames(t *testing.T) { + fakeClient := fake.NewClientBuilder(). + WithScheme(completionTestScheme(t)). + WithObjects( + virtualClusterPolicy("foo"), + virtualClusterPolicy("bar"), + // policies are cluster scoped, so clusters and namespaces are not completed + namespace("k3k-foo"), + cluster("foo", "k3k-foo"), + ). + Build() + + names, directive := policyNameCompletions(t.Context(), fakeClient) + + assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) + assert.ElementsMatch(t, []string{"foo", "bar"}, names) +} + +func Test_completePolicyNames_empty(t *testing.T) { + fakeClient := fake.NewClientBuilder(). + WithScheme(completionTestScheme(t)). + Build() + + names, directive := policyNameCompletions(t.Context(), fakeClient) + + // no policies is not an error + assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) + assert.Empty(t, names) +} + +func Test_completeClusterMode(t *testing.T) { + modes, directive := completeClusterMode(&cobra.Command{}, nil, "") + + assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive) + // every mode accepted by validClusterModes must be completed + assert.ElementsMatch(t, []string{"shared", "virtual", "hcp"}, modes) +} diff --git a/cli/cmds/policy_delete.go b/cli/cmds/policy_delete.go index 71937925..f23ac0d0 100644 --- a/cli/cmds/policy_delete.go +++ b/cli/cmds/policy_delete.go @@ -11,11 +11,12 @@ import ( func NewPolicyDeleteCmd(appCtx *AppContext) *cobra.Command { return &cobra.Command{ - Use: "delete", - Short: "Delete an existing policy.", - Example: "k3kcli policy delete [command options] NAME", - RunE: policyDeleteAction(appCtx), - Args: cobra.ExactArgs(1), + Use: "delete", + Short: "Delete an existing policy.", + Example: "k3kcli policy delete [command options] NAME", + RunE: policyDeleteAction(appCtx), + Args: cobra.ExactArgs(1), + ValidArgsFunction: completePolicyNameArg, } }