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)
}