commands/cluster/agent/update_kubeconfig/agent_update_kubeconfig.go (164 lines of code) (raw):

package update_kubeconfig import ( "fmt" "os" "strconv" "strings" "github.com/MakeNowJust/heredoc/v2" "github.com/spf13/cobra" gitlab "gitlab.com/gitlab-org/api/client-go" "gitlab.com/gitlab-org/cli/api" "gitlab.com/gitlab-org/cli/commands/cluster/agent/agentutils" "gitlab.com/gitlab-org/cli/commands/cmdutils" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) const ( k8sAuthInfoExecApiVersion = "client.authentication.k8s.io/v1" flagAgent = "agent" flagUseContext = "use-context" ) var sanitizeReplacer = strings.NewReplacer("/", "_", ".", "_") func NewCmdAgentUpdateKubeconfig(f *cmdutils.Factory) *cobra.Command { pathOptions := clientcmd.NewDefaultPathOptions() if len(pathOptions.ExplicitFileFlag) == 0 { pathOptions.ExplicitFileFlag = clientcmd.RecommendedConfigPathFlag } agentUpdateKubeconfigCmd := &cobra.Command{ Use: "update-kubeconfig [flags]", Short: `Update selected kubeconfig.`, Long: heredoc.Doc(`Update selected kubeconfig for use with a GitLab agent for Kubernetes. `), RunE: func(cmd *cobra.Command, args []string) error { agentID, err := cmd.Flags().GetInt(flagAgent) if err != nil { return err } useContext, err := cmd.Flags().GetBool(flagUseContext) if err != nil { return err } return runUpdateKubeconfig(agentID, pathOptions, useContext, f) }, } agentUpdateKubeconfigCmd.Flags().IntP(flagAgent, "a", 0, "The numeric agent ID to create the kubeconfig entry for.") cobra.CheckErr(agentUpdateKubeconfigCmd.MarkFlagRequired(flagAgent)) persistentFlags := agentUpdateKubeconfigCmd.PersistentFlags() persistentFlags.StringVar(&pathOptions.LoadingRules.ExplicitPath, pathOptions.ExplicitFileFlag, pathOptions.LoadingRules.ExplicitPath, "Use a particular kubeconfig file.") persistentFlags.BoolP(flagUseContext, "u", false, "Use as default context.") return agentUpdateKubeconfigCmd } func runUpdateKubeconfig(agentID int, configAccess clientcmd.ConfigAccess, useContext bool, factory *cmdutils.Factory) error { apiClient, err := factory.HttpClient() if err != nil { return err } repo, err := factory.BaseRepo() if err != nil { return err } // Retrieve metadata of the instance to determine KAS URL metadata, err := api.GetMetadata(apiClient) if err != nil { return err } if !metadata.KAS.Enabled { return fmt.Errorf("the GitLab agent server for Kubernetes is disabled on %s. Ask your administrator to enable and configure it.", repo.RepoHost()) } kasK8SProxyURL, err := agentutils.GetKasK8SProxyURL(metadata) if err != nil { return err } // Retrieve agent information, most importantly its name to use it as context name. agent, err := api.GetAgent(apiClient, repo.FullName(), agentID) if err != nil { return err } // Retrieve user information user, err := api.CurrentUser(apiClient) if err != nil { return err } // Retrieve glab executable path for exec glabExecutable, err := os.Executable() if err != nil { return nil } startingConfig, err := configAccess.GetStartingConfig() if err != nil { return err } params := updateKubeconfigParams{ startingConfig: *startingConfig, glabExecutable: glabExecutable, glHost: repo.RepoHost(), glUser: user.Username, kasK8sProxyURL: kasK8SProxyURL, agent: agent, } config, contextName := updateKubeconfig(params) if useContext { config.CurrentContext = contextName } if err := clientcmd.ModifyConfig(configAccess, config, true); err != nil { return err } fmt.Fprintf(factory.IO.StdOut, "Updated context %s.\n", contextName) if useContext { fmt.Fprintf(factory.IO.StdOut, "Using context %s.\n", contextName) } return nil } type updateKubeconfigParams struct { startingConfig clientcmdapi.Config glabExecutable string glHost string glUser string kasK8sProxyURL string agent *gitlab.Agent } func updateKubeconfig(params updateKubeconfigParams) (clientcmdapi.Config, string) { config := params.startingConfig // Updating `clusters` entry: `kubectl config set-cluster ...` clusterName := sanitizeForKubeconfig(params.glHost) startingCluster, exists := config.Clusters[clusterName] if !exists { startingCluster = clientcmdapi.NewCluster() } config.Clusters[clusterName] = modifyCluster(*startingCluster, params.kasK8sProxyURL) // Updating `users` entry: `kubectl config set-credentials ...` authInfoName := fmt.Sprintf("%s-%d", clusterName, params.agent.ID) startingAuthInfo, exists := config.AuthInfos[authInfoName] if !exists { startingAuthInfo = clientcmdapi.NewAuthInfo() } config.AuthInfos[authInfoName] = modifyAuthInfo(*startingAuthInfo, params.glabExecutable, params.agent.ID) // Updating `contexts` entry: `kubectl config set-context ...` contextName := fmt.Sprintf("%s-%s-%s", clusterName, sanitizeForKubeconfig(params.agent.ConfigProject.PathWithNamespace), params.agent.Name) startingContext, exists := config.Contexts[contextName] if !exists { startingContext = clientcmdapi.NewContext() } config.Contexts[contextName] = modifyContext(*startingContext, clusterName, authInfoName) return config, contextName } func modifyCluster(cluster clientcmdapi.Cluster, server string) *clientcmdapi.Cluster { cluster.Server = server return &cluster } func modifyAuthInfo(authInfo clientcmdapi.AuthInfo, glabExecutable string, agentID int) *clientcmdapi.AuthInfo { // Clear existing auth info authInfo.Token = "" authInfo.TokenFile = "" authInfo.Exec = &clientcmdapi.ExecConfig{ APIVersion: k8sAuthInfoExecApiVersion, Command: glabExecutable, Args: []string{"cluster", "agent", "get-token", "--agent", strconv.Itoa(agentID)}, InteractiveMode: clientcmdapi.NeverExecInteractiveMode, InstallHint: heredoc.Doc(` To authenticate to the current cluster, glab is required. Follow the installation instructions at https://gitlab.com/gitlab-org/cli#installation. `), } return &authInfo } func modifyContext(ctx clientcmdapi.Context, clusterName, authInfoName string) *clientcmdapi.Context { ctx.Cluster = clusterName ctx.AuthInfo = authInfoName return &ctx } func sanitizeForKubeconfig(name string) string { return sanitizeReplacer.Replace(name) }