package kubeconfig import ( "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // KubeConfig is the top-level kubeconfig structure. type KubeConfig struct { APIVersion string `yaml:"apiVersion"` Kind string `yaml:"kind"` Preferences map[string]any `yaml:"preferences,omitempty"` Clusters []NamedCluster `yaml:"clusters"` Users []NamedUser `yaml:"users"` Contexts []NamedContext `yaml:"contexts"` CurrentContext string `yaml:"current-context,omitempty"` } // NamedCluster is a named cluster entry. type NamedCluster struct { Name string `yaml:"name"` Cluster ClusterData `yaml:"cluster"` } // ClusterData holds the cluster connection details. type ClusterData struct { Server string `yaml:"server"` CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty"` } // NamedUser is a named user entry. type NamedUser struct { Name string `yaml:"name"` User UserData `yaml:"user"` } // UserData holds user credentials. type UserData struct { Exec *ExecData `yaml:"exec,omitempty"` } // ExecData holds exec plugin configuration. type ExecData struct { APIVersion string `yaml:"apiVersion"` Command string `yaml:"command"` Args []string `yaml:"args,omitempty"` InteractiveMode string `yaml:"interactiveMode,omitempty"` } // NamedContext is a named context entry. type NamedContext struct { Name string `yaml:"name"` Context ContextData `yaml:"context"` } // ContextData holds context details. type ContextData struct { Cluster string `yaml:"cluster"` User string `yaml:"user"` } // FilePath returns the path to the active kubeconfig file. // If KUBECONFIG is set to a colon-separated list, the first entry is returned. func FilePath() string { if k := os.Getenv("KUBECONFIG"); k != "" { if idx := strings.IndexByte(k, os.PathListSeparator); idx >= 0 { return k[:idx] } return k } home, _ := os.UserHomeDir() return filepath.Join(home, ".kube", "config") } // RenameContext renames the cluster and context (but not the user) in cfg. // The bootstrap template uses the cluster name as both the cluster and context // name; the user name is the actual username and is left unchanged. func RenameContext(cfg *KubeConfig, newName string) { oldName := cfg.CurrentContext if oldName == newName { return } for i := range cfg.Clusters { if cfg.Clusters[i].Name == oldName { cfg.Clusters[i].Name = newName } } for i := range cfg.Contexts { if cfg.Contexts[i].Name == oldName { cfg.Contexts[i].Name = newName cfg.Contexts[i].Context.Cluster = newName } } cfg.CurrentContext = newName } // Merge merges incoming into the kubeconfig file at FilePath(). // If setContext is true, the current-context is updated to incoming.CurrentContext. func Merge(incoming *KubeConfig, setContext bool) error { path := FilePath() existing := &KubeConfig{APIVersion: "v1", Kind: "Config"} if data, err := os.ReadFile(path); err == nil { if err := yaml.Unmarshal(data, existing); err != nil { return fmt.Errorf("parsing existing kubeconfig: %w", err) } } for _, c := range incoming.Clusters { existing.Clusters = upsertCluster(existing.Clusters, c) } for _, u := range incoming.Users { existing.Users = upsertUser(existing.Users, u) } for _, ctx := range incoming.Contexts { existing.Contexts = upsertContext(existing.Contexts, ctx) } if setContext { existing.CurrentContext = incoming.CurrentContext } data, err := yaml.Marshal(existing) if err != nil { return fmt.Errorf("marshaling kubeconfig: %w", err) } if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { return fmt.Errorf("creating kubeconfig directory: %w", err) } return os.WriteFile(path, data, 0600) } func upsertCluster(list []NamedCluster, item NamedCluster) []NamedCluster { for i, c := range list { if c.Name == item.Name { list[i] = item return list } } return append(list, item) } func upsertUser(list []NamedUser, item NamedUser) []NamedUser { for i, u := range list { if u.Name == item.Name { list[i] = item return list } } return append(list, item) } func upsertContext(list []NamedContext, item NamedContext) []NamedContext { for i, c := range list { if c.Name == item.Name { list[i] = item return list } } return append(list, item) }