github.com/oam-dev/kubevela@v1.9.11/references/cli/auth.go (about)

     1  /*
     2  Copyright 2022 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cli
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"github.com/spf13/cobra"
    25  	"golang.org/x/term"
    26  	corev1 "k8s.io/api/core/v1"
    27  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    28  	apitypes "k8s.io/apimachinery/pkg/types"
    29  	"k8s.io/client-go/kubernetes"
    30  	"k8s.io/client-go/tools/clientcmd"
    31  	"k8s.io/kubectl/pkg/util/i18n"
    32  	"k8s.io/kubectl/pkg/util/templates"
    33  
    34  	"github.com/oam-dev/kubevela/apis/types"
    35  	"github.com/oam-dev/kubevela/pkg/auth"
    36  	velacmd "github.com/oam-dev/kubevela/pkg/cmd"
    37  	cmdutil "github.com/oam-dev/kubevela/pkg/cmd/util"
    38  	"github.com/oam-dev/kubevela/pkg/multicluster"
    39  	"github.com/oam-dev/kubevela/pkg/utils/util"
    40  )
    41  
    42  // AuthCommandGroup commands for create resources or configuration
    43  func AuthCommandGroup(f velacmd.Factory, order string, streams util.IOStreams) *cobra.Command {
    44  	cmd := &cobra.Command{
    45  		Use:   "auth",
    46  		Short: i18n.T("Manage identity and authorizations."),
    47  		Annotations: map[string]string{
    48  			types.TagCommandType:  types.TypePlatform,
    49  			types.TagCommandOrder: order,
    50  		},
    51  	}
    52  	cmd.AddCommand(NewGenKubeConfigCommand(f, streams))
    53  	cmd.AddCommand(NewListPrivilegesCommand(f, streams))
    54  	cmd.AddCommand(NewGrantPrivilegesCommand(f, streams))
    55  	return cmd
    56  }
    57  
    58  // GenKubeConfigOptions options for create kubeconfig
    59  type GenKubeConfigOptions struct {
    60  	auth.Identity
    61  	util.IOStreams
    62  }
    63  
    64  // Complete .
    65  func (opt *GenKubeConfigOptions) Complete(f velacmd.Factory, cmd *cobra.Command) {
    66  	if opt.Identity.ServiceAccount != "" {
    67  		opt.Identity.ServiceAccountNamespace = velacmd.GetNamespace(f, cmd)
    68  	}
    69  	opt.Regularize()
    70  }
    71  
    72  // Validate .
    73  func (opt *GenKubeConfigOptions) Validate() error {
    74  	return opt.Identity.Validate()
    75  }
    76  
    77  // Run .
    78  func (opt *GenKubeConfigOptions) Run(f velacmd.Factory) error {
    79  	ctx := context.Background()
    80  	cli, err := kubernetes.NewForConfig(f.Config())
    81  	if err != nil {
    82  		return err
    83  	}
    84  	cfg, err := clientcmd.NewDefaultPathOptions().GetStartingConfig()
    85  	if err != nil {
    86  		return err
    87  	}
    88  	cfg, err = auth.GenerateKubeConfig(ctx, cli, cfg, opt.IOStreams.ErrOut, auth.KubeConfigWithIdentityGenerateOption(opt.Identity))
    89  	if err != nil {
    90  		return err
    91  	}
    92  	bs, err := clientcmd.Write(*cfg)
    93  	if err != nil {
    94  		return err
    95  	}
    96  	_, err = opt.Out.Write(bs)
    97  	return err
    98  }
    99  
   100  var (
   101  	genKubeConfigLong = templates.LongDesc(i18n.T(`
   102  		Generate kubeconfig for user
   103  
   104  		Generate a new kubeconfig with specified identity. By default, the generated kubeconfig 
   105  		will reuse the certificate-authority-data in the cluster config from the current used 
   106  		kubeconfig. All contexts, clusters and users that are not in use will not be included
   107  		in the generated kubeconfig.
   108  
   109  		To generate a new kubeconfig for given user and groups, use the --user and --group flag.
   110  		Multiple --group flags is allowed. The group kubevela:client is added to the groups by 
   111  		default. The identity in the current kubeconfig should be able to approve 
   112  		CertificateSigningRequest in the kubernetes cluster. See
   113  		https://kubernetes.io/docs/reference/access-authn-authz/certificate-signing-requests/
   114  		for details.
   115  
   116  		To generate a kubeconfig based on existing ServiceAccount in your cluster, use the 
   117  		--serviceaccount flag. The corresponding secret token and ca data will be embedded in 
   118  		the generated kubeconfig, which allows you to act as the serviceaccount.`))
   119  
   120  	generateKubeConfigExample = templates.Examples(i18n.T(`
   121  		# Generate a kubeconfig with provided user
   122  		vela auth gen-kubeconfig --user new-user
   123  		
   124  		# Generate a kubeconfig with provided user and group
   125  		vela auth gen-kubeconfig --user new-user --group kubevela:developer
   126  		
   127  		# Generate a kubeconfig with provided user and groups
   128  		vela auth gen-kubeconfig --user new-user --group kubevela:developer --group my-org:my-team
   129  
   130  		# Generate a kubeconfig with provided serviceaccount
   131  		vela auth gen-kubeconfig --serviceaccount default -n demo`))
   132  )
   133  
   134  // NewGenKubeConfigCommand generate kubeconfig for given user and groups
   135  func NewGenKubeConfigCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
   136  	o := &GenKubeConfigOptions{IOStreams: streams}
   137  	cmd := &cobra.Command{
   138  		Use:                   "gen-kubeconfig",
   139  		DisableFlagsInUseLine: true,
   140  		Short:                 i18n.T("Generate kubeconfig for user"),
   141  		Long:                  genKubeConfigLong,
   142  		Example:               generateKubeConfigExample,
   143  		Annotations: map[string]string{
   144  			types.TagCommandType: types.TypeCD,
   145  		},
   146  		Args: cobra.ExactArgs(0),
   147  		Run: func(cmd *cobra.Command, args []string) {
   148  			o.Complete(f, cmd)
   149  			cmdutil.CheckErr(o.Validate())
   150  			cmdutil.CheckErr(o.Run(f))
   151  		},
   152  	}
   153  	cmd.Flags().StringVarP(&o.User, "user", "u", o.User, "The user of the generated kubeconfig. If set, an X509-based kubeconfig will be intended to create. It will be embedded as the Subject in the X509 certificate.")
   154  	cmd.Flags().StringSliceVarP(&o.Groups, "group", "g", o.Groups, "The groups of the generated kubeconfig. This flag only works when `--user` is set. It will be embedded as the Organization in the X509 certificate.")
   155  	cmd.Flags().StringVarP(&o.ServiceAccount, "serviceaccount", "", o.ServiceAccount, "The serviceaccount of the generated kubeconfig. If set, a kubeconfig will be generated based on the secret token of the serviceaccount. Cannot be set when `--user` presents.")
   156  	cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
   157  		"serviceaccount", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   158  			if strings.TrimSpace(o.User) != "" {
   159  				return nil, cobra.ShellCompDirectiveNoFileComp
   160  			}
   161  			namespace := velacmd.GetNamespace(f, cmd)
   162  			return velacmd.GetServiceAccountForCompletion(cmd.Context(), f, namespace, toComplete)
   163  		}))
   164  
   165  	return velacmd.NewCommandBuilder(f, cmd).
   166  		WithNamespaceFlag(velacmd.UsageOption("The namespace of the serviceaccount. This flag only works when `--serviceaccount` is set.")).
   167  		WithStreams(streams).
   168  		WithResponsiveWriter().
   169  		Build()
   170  }
   171  
   172  // ListPrivilegesOptions options for list privileges
   173  type ListPrivilegesOptions struct {
   174  	auth.Identity
   175  	KubeConfig string
   176  	Clusters   []string
   177  	util.IOStreams
   178  }
   179  
   180  // Complete .
   181  func (opt *ListPrivilegesOptions) Complete(f velacmd.Factory, cmd *cobra.Command) {
   182  	if opt.KubeConfig != "" {
   183  		identity, err := auth.ReadIdentityFromKubeConfig(opt.KubeConfig)
   184  		cmdutil.CheckErr(err)
   185  		opt.Identity = *identity
   186  	}
   187  	if opt.Identity.ServiceAccount != "" {
   188  		opt.Identity.ServiceAccountNamespace = velacmd.GetNamespace(f, cmd)
   189  	}
   190  	opt.Clusters = velacmd.GetClusters(cmd)
   191  	opt.Regularize()
   192  }
   193  
   194  // Validate .
   195  func (opt *ListPrivilegesOptions) Validate(f velacmd.Factory, cmd *cobra.Command) error {
   196  	if err := opt.Identity.Validate(); err != nil {
   197  		return err
   198  	}
   199  	for _, cluster := range opt.Clusters {
   200  		if _, err := multicluster.NewClusterClient(f.Client()).Get(cmd.Context(), cluster); err != nil {
   201  			return fmt.Errorf("failed to find cluster %s: %w", cluster, err)
   202  		}
   203  	}
   204  	return nil
   205  }
   206  
   207  // Run .
   208  func (opt *ListPrivilegesOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
   209  	ctx := cmd.Context()
   210  	m, err := auth.ListPrivileges(ctx, f.Client(), opt.Clusters, &opt.Identity)
   211  	if err != nil {
   212  		return err
   213  	}
   214  	width, _, err := term.GetSize(0)
   215  	if err != nil {
   216  		width = 80
   217  	}
   218  	_, _ = opt.Out.Write([]byte(auth.PrettyPrintPrivileges(&opt.Identity, m, opt.Clusters, uint(width)-40)))
   219  	return nil
   220  }
   221  
   222  var (
   223  	listPrivilegesLong = templates.LongDesc(i18n.T(`
   224  		List privileges for user
   225  
   226  		List privileges that user has in clusters. Use --user/--group to check the privileges
   227  		for specified user and group. They can be jointly configured to see the union of
   228  		privileges. Use --serviceaccount and -n/--namespace to see the privileges for 
   229  		ServiceAccount. You can also use --kubeconfig to use the identity inside implicitly. 
   230  		The privileges will be shown in tree format.
   231  
   232  		This command supports listing privileges across multiple clusters, by using --cluster.
   233  		If not set, the control plane will be used. This feature requires cluster-gateway to be
   234  		properly setup to use. 
   235  
   236  		The privileges are collected through listing all ClusterRoleBinding and RoleBinding,
   237  		following the Kubernetes RBAC Authorization. Other authorization mechanism is not supported
   238  		now. See https://kubernetes.io/docs/reference/access-authn-authz/rbac/ for details.
   239  		
   240  		The ClusterRoleBinding and RoleBinding that matches the specified identity will be 
   241  		tracked. Related ClusterRoles and Roles are retrieved and the contained PolicyRules are
   242  		demonstrated.`))
   243  
   244  	listPrivilegesExample = templates.Examples(i18n.T(`
   245  		# List privileges for User alice in the control plane
   246  		vela auth list-privileges --user alice
   247  		
   248  		# List privileges for Group org:dev-team in the control plane
   249  		vela auth list-privileges --group org:dev-team
   250  
   251  		# List privileges for User bob with Groups org:dev-team and org:test-team in the control plane and managed cluster example-cluster
   252  		vela auth list-privileges --user bob --group org:dev-team --group org:test-team --cluster local --cluster example-cluster
   253  		
   254  		# List privileges for ServiceAccount example-sa in demo namespace in multiple managed clusters 
   255  		vela auth list-privileges --serviceaccount example-sa -n demo --cluster cluster-1 --cluster cluster-2
   256  
   257  		# List privileges for identity in kubeconfig
   258  		vela auth list-privileges --kubeconfig ./example.kubeconfig --cluster local --cluster cluster-1`))
   259  )
   260  
   261  // NewListPrivilegesCommand list privileges for given identity
   262  func NewListPrivilegesCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
   263  	o := &ListPrivilegesOptions{IOStreams: streams}
   264  	cmd := &cobra.Command{
   265  		Use:                   "list-privileges",
   266  		DisableFlagsInUseLine: true,
   267  		Short:                 i18n.T("List privileges for user/group/serviceaccount"),
   268  		Long:                  listPrivilegesLong,
   269  		Example:               listPrivilegesExample,
   270  		Annotations: map[string]string{
   271  			types.TagCommandType: types.TypeCD,
   272  		},
   273  		Args: cobra.ExactArgs(0),
   274  		Run: func(cmd *cobra.Command, args []string) {
   275  			o.Complete(f, cmd)
   276  			cmdutil.CheckErr(o.Validate(f, cmd))
   277  			cmdutil.CheckErr(o.Run(f, cmd))
   278  		},
   279  	}
   280  	cmd.Flags().StringVarP(&o.User, "user", "u", o.User, "The user to list privileges.")
   281  	cmd.Flags().StringSliceVarP(&o.Groups, "group", "g", o.Groups, "The group to list privileges. Can be set together with --user.")
   282  	cmd.Flags().StringVarP(&o.ServiceAccount, "serviceaccount", "", o.ServiceAccount, "The serviceaccount to list privileges. Cannot be set with --user and --group.")
   283  	cmd.Flags().StringVarP(&o.KubeConfig, "kubeconfig", "", o.KubeConfig, "The kubeconfig to list privileges. If set, it will override all the other identity flags.")
   284  	cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
   285  		"serviceaccount", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   286  			if strings.TrimSpace(o.User) != "" {
   287  				return nil, cobra.ShellCompDirectiveNoFileComp
   288  			}
   289  			namespace := velacmd.GetNamespace(f, cmd)
   290  			return velacmd.GetServiceAccountForCompletion(cmd.Context(), f, namespace, toComplete)
   291  		}))
   292  
   293  	return velacmd.NewCommandBuilder(f, cmd).
   294  		WithNamespaceFlag(velacmd.UsageOption("The namespace of the serviceaccount. This flag only works when `--serviceaccount` is set.")).
   295  		WithClusterFlag(velacmd.UsageOption("The cluster to list privileges. If not set, the command will list privileges in the control plane.")).
   296  		WithStreams(streams).
   297  		WithResponsiveWriter().
   298  		Build()
   299  }
   300  
   301  // GrantPrivilegesOptions options for grant privileges
   302  type GrantPrivilegesOptions struct {
   303  	auth.Identity
   304  	KubeConfig      string
   305  	GrantNamespaces []string
   306  	GrantClusters   []string
   307  	ReadOnly        bool
   308  	CreateNamespace bool
   309  
   310  	util.IOStreams
   311  }
   312  
   313  // Complete .
   314  func (opt *GrantPrivilegesOptions) Complete(f velacmd.Factory, cmd *cobra.Command) {
   315  	if opt.KubeConfig != "" {
   316  		identity, err := auth.ReadIdentityFromKubeConfig(opt.KubeConfig)
   317  		cmdutil.CheckErr(err)
   318  		opt.Identity = *identity
   319  		opt.Identity.Groups = nil
   320  	}
   321  	if opt.Identity.ServiceAccount != "" {
   322  		opt.Identity.ServiceAccountNamespace = velacmd.GetNamespace(f, cmd)
   323  	}
   324  	opt.Regularize()
   325  	if len(opt.GrantClusters) == 0 {
   326  		opt.GrantClusters = []string{types.ClusterLocalName}
   327  	}
   328  }
   329  
   330  // Validate .
   331  func (opt *GrantPrivilegesOptions) Validate(f velacmd.Factory, cmd *cobra.Command) error {
   332  	if opt.User == "" && len(opt.Groups) == 0 && opt.ServiceAccount == "" {
   333  		return fmt.Errorf("at least one idenity (user/group/serviceaccount) should be set")
   334  	}
   335  	for _, cluster := range opt.GrantClusters {
   336  		if _, err := multicluster.NewClusterClient(f.Client()).Get(cmd.Context(), cluster); err != nil {
   337  			return fmt.Errorf("failed to find cluster %s: %w", cluster, err)
   338  		}
   339  		if !opt.CreateNamespace {
   340  			for _, namespace := range opt.GrantNamespaces {
   341  				if err := f.Client().Get(multicluster.ContextWithClusterName(cmd.Context(), cluster), apitypes.NamespacedName{Name: namespace}, &corev1.Namespace{}); err != nil {
   342  					return fmt.Errorf("failed to find namespace %s in cluster %s: %w", namespace, cluster, err)
   343  				}
   344  			}
   345  		}
   346  	}
   347  	return nil
   348  }
   349  
   350  // Run .
   351  func (opt *GrantPrivilegesOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
   352  	ctx := cmd.Context()
   353  	if opt.CreateNamespace {
   354  		for _, cluster := range opt.GrantClusters {
   355  			if _, err := multicluster.NewClusterClient(f.Client()).Get(cmd.Context(), cluster); err != nil {
   356  				return fmt.Errorf("failed to find cluster %s: %w", cluster, err)
   357  			}
   358  			for _, namespace := range opt.GrantNamespaces {
   359  				_ctx := multicluster.ContextWithClusterName(cmd.Context(), cluster)
   360  				ns := &corev1.Namespace{}
   361  				if err := f.Client().Get(_ctx, apitypes.NamespacedName{Name: namespace}, ns); err != nil {
   362  					if kerrors.IsNotFound(err) {
   363  						ns.SetName(namespace)
   364  						if err = f.Client().Create(_ctx, ns); err != nil {
   365  							return fmt.Errorf("failed to create namespace %s in cluster %s: %w", namespace, cluster, err)
   366  						}
   367  						continue
   368  					}
   369  					return fmt.Errorf("failed to find namespace %s in cluster %s: %w", namespace, cluster, err)
   370  				}
   371  			}
   372  		}
   373  	}
   374  	var privileges []auth.PrivilegeDescription
   375  	for _, cluster := range opt.GrantClusters {
   376  		for _, namespace := range opt.GrantNamespaces {
   377  			privileges = append(privileges, &auth.ScopedPrivilege{Cluster: cluster, Namespace: namespace, ReadOnly: opt.ReadOnly})
   378  		}
   379  		if len(opt.GrantNamespaces) == 0 {
   380  			privileges = append(privileges, &auth.ScopedPrivilege{Cluster: cluster, ReadOnly: opt.ReadOnly})
   381  		}
   382  	}
   383  	if err := auth.GrantPrivileges(ctx, f.Client(), privileges, &opt.Identity, opt.IOStreams.Out); err != nil {
   384  		return err
   385  	}
   386  	_, _ = fmt.Fprintf(opt.IOStreams.Out, "Privileges granted.\n")
   387  	return nil
   388  }
   389  
   390  var (
   391  	grantPrivilegesLong = templates.LongDesc(i18n.T(`
   392  		Grant privileges for user
   393  
   394  		Grant privileges to user/group/serviceaccount. By using --for-namespace and --for-cluster,
   395  		you can grant all read/write privileges for all resources in the specified namespace and 
   396  		cluster. If --for-namespace is not set, the privileges will be granted cluster-wide. 
   397  
   398  		Setting --create-namespace will automatically create namespace if the namespace of the
   399  		granted privilege does not exists. By default, this flag is not enabled and errors will be
   400  		returned if the namespace is not found in the corresponding cluster.
   401  
   402  		Setting --readonly will only grant read privileges for all resources in the destination. This
   403  		can be useful if you want to give somebody the privileges to view resources but do not want to
   404  		allow them to edit any resource.
   405  		
   406  		If multiple identity information are set, all the identity information will be bond to the
   407  		intended privileges respectively.
   408  
   409  		If --kubeconfig is set, the user/serviceaccount information in the kubeconfig will be used as
   410  		the identity to grant privileges. Groups will be ignored.`))
   411  
   412  	grantPrivilegesExample = templates.Examples(i18n.T(`
   413  		# Grant privileges for User alice in the namespace demo of the control plane
   414  		vela auth grant-privileges --user alice --for-namespace demo
   415  
   416  		# Grant privileges for User alice in the namespace demo in cluster-1, create demo namespace if not exist
   417  		vela auth grant-privileges --user alice --for-namespace demo --for-cluster cluster-1 --create-namespace
   418  		
   419  		# Grant cluster-scoped privileges for Group org:dev-team in the control plane
   420  		vela auth grant-privileges --group org:dev-team
   421  
   422  		# Grant privileges for Group org:dev-team and org:test-team in the namespace test on the control plane and managed cluster example-cluster
   423  		vela auth grant-privileges --group org:dev-team --group org:test-team --for-namespace test --for-cluster local --for-cluster example-cluster
   424  		
   425  		# Grant read privileges for ServiceAccount observer in test namespace on the control plane
   426  		vela auth grant-privileges --serviceaccount observer -n test --for-namespace test --readonly
   427  
   428  		# Grant privileges for identity in kubeconfig in cluster-1
   429  		vela auth grant-privileges --kubeconfig ./example.kubeconfig --for-cluster cluster-1`))
   430  )
   431  
   432  // NewGrantPrivilegesCommand grant privileges to given identity
   433  func NewGrantPrivilegesCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
   434  	o := &GrantPrivilegesOptions{IOStreams: streams}
   435  	cmd := &cobra.Command{
   436  		Use:                   "grant-privileges",
   437  		DisableFlagsInUseLine: true,
   438  		Short:                 i18n.T("Grant privileges for user/group/serviceaccount"),
   439  		Long:                  grantPrivilegesLong,
   440  		Example:               grantPrivilegesExample,
   441  		Annotations: map[string]string{
   442  			types.TagCommandType: types.TypeCD,
   443  		},
   444  		Args: cobra.ExactArgs(0),
   445  		Run: func(cmd *cobra.Command, args []string) {
   446  			o.Complete(f, cmd)
   447  			cmdutil.CheckErr(o.Validate(f, cmd))
   448  			cmdutil.CheckErr(o.Run(f, cmd))
   449  		},
   450  	}
   451  	cmd.Flags().StringVarP(&o.User, "user", "u", o.User, "The user to grant privileges.")
   452  	cmd.Flags().StringSliceVarP(&o.Groups, "group", "g", o.Groups, "The group to grant privileges.")
   453  	cmd.Flags().StringVarP(&o.ServiceAccount, "serviceaccount", "", o.ServiceAccount, "The serviceaccount to grant privileges.")
   454  	cmd.Flags().StringVarP(&o.KubeConfig, "kubeconfig", "", o.KubeConfig, "The kubeconfig to grant privileges. If set, it will override all the other identity flags.")
   455  	cmd.Flags().StringSliceVarP(&o.GrantClusters, "for-cluster", "", o.GrantClusters, "The clusters privileges to grant. If empty, the control plane will be used.")
   456  	cmd.Flags().StringSliceVarP(&o.GrantNamespaces, "for-namespace", "", o.GrantNamespaces, "The namespaces privileges to grant. If empty, cluster-scoped privileges will be granted.")
   457  	cmd.Flags().BoolVarP(&o.ReadOnly, "readonly", "", o.ReadOnly, "If set, only read privileges of resources will be granted. Otherwise, read/write privileges will be granted.")
   458  	cmd.Flags().BoolVarP(&o.CreateNamespace, "create-namespace", "", o.CreateNamespace, "If set, non-exist namespace will be created automatically.")
   459  	cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
   460  		"serviceaccount", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   461  			if strings.TrimSpace(o.User) != "" {
   462  				return nil, cobra.ShellCompDirectiveNoFileComp
   463  			}
   464  			namespace := velacmd.GetNamespace(f, cmd)
   465  			return velacmd.GetServiceAccountForCompletion(cmd.Context(), f, namespace, toComplete)
   466  		}))
   467  
   468  	return velacmd.NewCommandBuilder(f, cmd).
   469  		WithNamespaceFlag(velacmd.UsageOption("The namespace of the serviceaccount. This flag only works when `--serviceaccount` is set.")).
   470  		WithStreams(streams).
   471  		WithResponsiveWriter().
   472  		Build()
   473  }