diff --git a/pkg/agent/kube/client.go b/pkg/agent/kube/client.go
index 09a3b17ea9..c0ea668556 100644
--- a/pkg/agent/kube/client.go
+++ b/pkg/agent/kube/client.go
@@ -5,11 +5,9 @@ import (
"context"
"encoding/base64"
"fmt"
- "net/url"
"os"
"os/exec"
"path/filepath"
- "reflect"
"strings"
"sync"
"time"
@@ -450,214 +448,6 @@ func (k *KubectlProxy) RenameContext(oldName, newName string) error {
return nil
}
-// KubeconfigPreviewEntry describes a context found in an imported kubeconfig.
-type KubeconfigPreviewEntry struct {
- ContextName string `json:"contextName"`
- ClusterName string `json:"clusterName"`
- ServerURL string `json:"serverUrl"`
- UserName string `json:"userName"`
- AuthMethod string `json:"authMethod,omitempty"` // exec, token, certificate, auth-provider, unknown
- IsNew bool `json:"isNew"`
-}
-
-// PreviewKubeconfig parses a kubeconfig YAML and returns the contexts it contains
-// along with whether each would be new or already exists.
-// SECURITY: AuthInfo entries with Exec plugins are flagged with auth method "exec (blocked)".
-func (k *KubectlProxy) PreviewKubeconfig(yamlContent string) ([]KubeconfigPreviewEntry, error) {
- k.mu.RLock()
- defer k.mu.RUnlock()
-
- incoming, err := clientcmd.Load([]byte(yamlContent))
- if err != nil {
- return nil, fmt.Errorf("invalid kubeconfig YAML: %w", err)
- }
- if len(incoming.Contexts) == 0 {
- return nil, fmt.Errorf("kubeconfig contains no contexts")
- }
-
- entries := make([]KubeconfigPreviewEntry, 0)
- for name, ctx := range incoming.Contexts {
- entry := KubeconfigPreviewEntry{
- ContextName: name,
- ClusterName: ctx.Cluster,
- UserName: ctx.AuthInfo,
- AuthMethod: detectAuthMethod(incoming.AuthInfos[ctx.AuthInfo]),
- }
- if cluster, ok := incoming.Clusters[ctx.Cluster]; ok {
- entry.ServerURL = cluster.Server
- }
- _, exists := k.config.Contexts[name]
- entry.IsNew = !exists
- entries = append(entries, entry)
- }
- return entries, nil
-}
-
-// ImportKubeconfig merges a kubeconfig YAML string into the existing kubeconfig file.
-// It backs up the existing file first, then merges new contexts/clusters/users.
-// Returns lists of added and skipped context names.
-//
-// SECURITY: AuthInfo entries with Exec plugins are rejected to prevent RCE (#7260).
-func (k *KubectlProxy) ImportKubeconfig(yamlContent string) (added []string, skipped []string, err error) {
- incoming, err := clientcmd.Load([]byte(yamlContent))
- if err != nil {
- return nil, nil, fmt.Errorf("invalid kubeconfig YAML: %w", err)
- }
- if len(incoming.Contexts) == 0 {
- return nil, nil, fmt.Errorf("kubeconfig contains no contexts")
- }
-
- // SECURITY: Reject any AuthInfo with an exec plugin — uploading a
- // kubeconfig with exec.command = "/bin/sh" achieves RCE (#7260).
- for name, ai := range incoming.AuthInfos {
- if ai != nil && ai.Exec != nil {
- return nil, nil, fmt.Errorf("SECURITY: kubeconfig user %q uses exec-based auth (command: %s) — exec plugins are not allowed for imported configs", name, ai.Exec.Command)
- }
- }
-
- k.mu.Lock()
- defer k.mu.Unlock()
-
- // Backup existing kubeconfig if the file exists.
- // Uses UnixNano to avoid collisions from concurrent imports (#7276).
- if _, statErr := os.Stat(k.kubeconfig); statErr == nil {
- backupPath := fmt.Sprintf("%s.bak-%d", k.kubeconfig, time.Now().UnixNano())
- data, readErr := os.ReadFile(k.kubeconfig)
- if readErr != nil {
- return nil, nil, fmt.Errorf("failed to read kubeconfig for backup: %w", readErr)
- }
- if writeErr := os.WriteFile(backupPath, data, 0600); writeErr != nil {
- return nil, nil, fmt.Errorf("failed to write backup: %w", writeErr)
- }
- }
-
- // Initialise maps if they are nil (empty starting config)
- if k.config.Contexts == nil {
- k.config.Contexts = make(map[string]*api.Context)
- }
- if k.config.Clusters == nil {
- k.config.Clusters = make(map[string]*api.Cluster)
- }
- if k.config.AuthInfos == nil {
- k.config.AuthInfos = make(map[string]*api.AuthInfo)
- }
-
- for name, ctx := range incoming.Contexts {
- if _, exists := k.config.Contexts[name]; exists {
- skipped = append(skipped, name)
- continue
- }
-
- // Resolve cluster name collisions: if the name already exists with
- // different data, pick a unique name so we don't silently drop the
- // incoming cluster definition.
- clusterName := ctx.Cluster
- if incomingCluster, ok := incoming.Clusters[clusterName]; ok {
- if existing, exists := k.config.Clusters[clusterName]; exists {
- if !clustersEquivalent(existing, incomingCluster) {
- clusterName = uniqueName(clusterName, k.config.Clusters)
- }
- // else: same data, reuse existing entry
- }
- }
-
- // Resolve user/auth-info name collisions the same way.
- userName := ctx.AuthInfo
- if incomingUser, ok := incoming.AuthInfos[userName]; ok {
- if existing, exists := k.config.AuthInfos[userName]; exists {
- if !authInfosEquivalent(existing, incomingUser) {
- userName = uniqueName(userName, k.config.AuthInfos)
- }
- }
- }
-
- // Build the context with possibly-renamed references.
- mergedCtx := ctx.DeepCopy()
- mergedCtx.Cluster = clusterName
- mergedCtx.AuthInfo = userName
- k.config.Contexts[name] = mergedCtx
-
- // Add referenced cluster if present
- if cluster, ok := incoming.Clusters[ctx.Cluster]; ok {
- if _, exists := k.config.Clusters[clusterName]; !exists {
- k.config.Clusters[clusterName] = cluster
- }
- }
- // Add referenced user if present
- if user, ok := incoming.AuthInfos[ctx.AuthInfo]; ok {
- if _, exists := k.config.AuthInfos[userName]; !exists {
- k.config.AuthInfos[userName] = user
- }
- }
- added = append(added, name)
- }
-
- // Write merged config
- if writeErr := clientcmd.WriteToFile(*k.config, k.kubeconfig); writeErr != nil {
- return nil, nil, fmt.Errorf("failed to write merged kubeconfig: %w", writeErr)
- }
-
- // Reload from file to stay in sync (already holding lock, use internal variant)
- k.reloadLocked()
-
- return added, skipped, nil
-}
-
-// clustersEquivalent returns true if two Cluster structs carry the same
-// semantic configuration. The LocationOfOrigin field is ignored because it
-// reflects which file a value was loaded from, not the cluster definition.
-func clustersEquivalent(a, b *api.Cluster) bool {
- if a == nil || b == nil {
- return a == b
- }
- ac := a.DeepCopy()
- bc := b.DeepCopy()
- ac.LocationOfOrigin = ""
- bc.LocationOfOrigin = ""
- return reflect.DeepEqual(ac, bc)
-}
-
-// authInfosEquivalent is the AuthInfo analogue of clustersEquivalent.
-func authInfosEquivalent(a, b *api.AuthInfo) bool {
- if a == nil || b == nil {
- return a == b
- }
- ac := a.DeepCopy()
- bc := b.DeepCopy()
- ac.LocationOfOrigin = ""
- bc.LocationOfOrigin = ""
- return reflect.DeepEqual(ac, bc)
-}
-
-// uniqueName returns a name that does not collide with any key in m.
-// It tries "-imported", then "-imported-2", "-imported-3", etc.
-func uniqueName[V any](base string, m map[string]V) string {
- candidate := base + "-imported"
- if _, exists := m[candidate]; !exists {
- return candidate
- }
- for i := 2; ; i++ {
- candidate = fmt.Sprintf("%s-imported-%d", base, i)
- if _, exists := m[candidate]; !exists {
- return candidate
- }
- }
-}
-
-// AddClusterRequest describes the form fields for adding a cluster.
-type AddClusterRequest struct {
- ContextName string `json:"contextName"`
- ClusterName string `json:"clusterName"`
- ServerURL string `json:"serverUrl"`
- AuthType string `json:"authType"` // "token", "certificate"
- Token string `json:"token,omitempty"`
- CertData string `json:"certData,omitempty"` // base64 PEM
- KeyData string `json:"keyData,omitempty"` // base64 PEM
- CAData string `json:"caData,omitempty"` // base64 PEM CA cert
- SkipTLSVerify bool `json:"skipTlsVerify,omitempty"`
- Namespace string `json:"namespace,omitempty"` // default namespace
-}
-
// TestConnectionRequest describes the fields for testing a cluster connection.
type TestConnectionRequest struct {
ServerURL string `json:"serverUrl"`
@@ -676,125 +466,6 @@ type TestConnectionResult struct {
Error string `json:"error,omitempty"`
}
-// AddCluster builds a kubeconfig entry from structured input and merges it.
-// Uses mutex for thread safety (#7259) and UnixNano for backup paths (#7276).
-func (k *KubectlProxy) AddCluster(req AddClusterRequest) error {
- k.mu.Lock()
- defer k.mu.Unlock()
-
- // Validate required fields
- if req.ContextName == "" || req.ClusterName == "" || req.ServerURL == "" || req.AuthType == "" {
- return fmt.Errorf("contextName, clusterName, serverUrl, and authType are required")
- }
-
- // Validate server URL format
- parsedURL, err := url.Parse(req.ServerURL)
- if err != nil {
- return fmt.Errorf("invalid server URL: %w", err)
- }
- if parsedURL.Scheme == "" || parsedURL.Host == "" {
- return fmt.Errorf("server URL must include a scheme and host (e.g. https://api.example.com:6443)")
- }
-
- // Validate auth-type-specific fields
- switch req.AuthType {
- case "token":
- if req.Token == "" {
- return fmt.Errorf("token is required for token auth type")
- }
- case "certificate":
- if req.CertData == "" || req.KeyData == "" {
- return fmt.Errorf("certData and keyData are required for certificate auth type")
- }
- default:
- return fmt.Errorf("unsupported authType: %s (must be token or certificate)", req.AuthType)
- }
-
- // Check context doesn't already exist
- if k.config.Contexts != nil {
- if _, exists := k.config.Contexts[req.ContextName]; exists {
- return fmt.Errorf("context %q already exists", req.ContextName)
- }
- }
-
- // Build cluster entry
- cluster := &api.Cluster{
- Server: req.ServerURL,
- InsecureSkipTLSVerify: req.SkipTLSVerify,
- }
- if req.CAData != "" {
- caBytes, err := base64.StdEncoding.DecodeString(req.CAData)
- if err != nil {
- return fmt.Errorf("invalid caData base64: %w", err)
- }
- cluster.CertificateAuthorityData = caBytes
- }
-
- // Build auth info entry
- userName := req.ContextName + "-user"
- authInfo := &api.AuthInfo{}
- switch req.AuthType {
- case "token":
- authInfo.Token = req.Token
- case "certificate":
- certBytes, err := base64.StdEncoding.DecodeString(req.CertData)
- if err != nil {
- return fmt.Errorf("invalid certData base64: %w", err)
- }
- keyBytes, err := base64.StdEncoding.DecodeString(req.KeyData)
- if err != nil {
- return fmt.Errorf("invalid keyData base64: %w", err)
- }
- authInfo.ClientCertificateData = certBytes
- authInfo.ClientKeyData = keyBytes
- }
-
- // Build context entry
- ctx := &api.Context{
- Cluster: req.ClusterName,
- AuthInfo: userName,
- Namespace: req.Namespace,
- }
-
- // Backup existing kubeconfig if the file exists.
- // Uses UnixNano to avoid collisions from concurrent imports (#7276).
- if _, statErr := os.Stat(k.kubeconfig); statErr == nil {
- backupPath := fmt.Sprintf("%s.bak-%d", k.kubeconfig, time.Now().UnixNano())
- data, readErr := os.ReadFile(k.kubeconfig)
- if readErr != nil {
- return fmt.Errorf("failed to read kubeconfig for backup: %w", readErr)
- }
- if writeErr := os.WriteFile(backupPath, data, 0600); writeErr != nil {
- return fmt.Errorf("failed to write backup: %w", writeErr)
- }
- }
-
- // Initialise maps if nil
- if k.config.Contexts == nil {
- k.config.Contexts = make(map[string]*api.Context)
- }
- if k.config.Clusters == nil {
- k.config.Clusters = make(map[string]*api.Cluster)
- }
- if k.config.AuthInfos == nil {
- k.config.AuthInfos = make(map[string]*api.AuthInfo)
- }
-
- // Add entries
- k.config.Clusters[req.ClusterName] = cluster
- k.config.AuthInfos[userName] = authInfo
- k.config.Contexts[req.ContextName] = ctx
-
- // Write to file
- if writeErr := clientcmd.WriteToFile(*k.config, k.kubeconfig); writeErr != nil {
- return fmt.Errorf("failed to write kubeconfig: %w", writeErr)
- }
-
- // Reload (already holding lock, use internal variant)
- k.reloadLocked()
- return nil
-}
-
// TestClusterConnection attempts to connect to a Kubernetes API server
// and returns basic info (version, reachable status).
func (k *KubectlProxy) TestClusterConnection(req TestConnectionRequest) (*TestConnectionResult, error) {
diff --git a/pkg/agent/kube/client_kubeconfig.go b/pkg/agent/kube/client_kubeconfig.go
new file mode 100644
index 0000000000..8b8d776e80
--- /dev/null
+++ b/pkg/agent/kube/client_kubeconfig.go
@@ -0,0 +1,340 @@
+package kube
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/url"
+ "os"
+ "reflect"
+ "time"
+
+ "k8s.io/client-go/tools/clientcmd"
+ "k8s.io/client-go/tools/clientcmd/api"
+)
+
+// KubeconfigPreviewEntry describes a context found in an imported kubeconfig.
+type KubeconfigPreviewEntry struct {
+ ContextName string `json:"contextName"`
+ ClusterName string `json:"clusterName"`
+ ServerURL string `json:"serverUrl"`
+ UserName string `json:"userName"`
+ AuthMethod string `json:"authMethod,omitempty"` // exec, token, certificate, auth-provider, unknown
+ IsNew bool `json:"isNew"`
+}
+
+// PreviewKubeconfig parses a kubeconfig YAML and returns the contexts it contains
+// along with whether each would be new or already exists.
+// SECURITY: AuthInfo entries with Exec plugins are flagged with auth method "exec (blocked)".
+func (k *KubectlProxy) PreviewKubeconfig(yamlContent string) ([]KubeconfigPreviewEntry, error) {
+ k.mu.RLock()
+ defer k.mu.RUnlock()
+
+ incoming, err := clientcmd.Load([]byte(yamlContent))
+ if err != nil {
+ return nil, fmt.Errorf("invalid kubeconfig YAML: %w", err)
+ }
+ if len(incoming.Contexts) == 0 {
+ return nil, fmt.Errorf("kubeconfig contains no contexts")
+ }
+
+ entries := make([]KubeconfigPreviewEntry, 0)
+ for name, ctx := range incoming.Contexts {
+ entry := KubeconfigPreviewEntry{
+ ContextName: name,
+ ClusterName: ctx.Cluster,
+ UserName: ctx.AuthInfo,
+ AuthMethod: detectAuthMethod(incoming.AuthInfos[ctx.AuthInfo]),
+ }
+ if cluster, ok := incoming.Clusters[ctx.Cluster]; ok {
+ entry.ServerURL = cluster.Server
+ }
+ _, exists := k.config.Contexts[name]
+ entry.IsNew = !exists
+ entries = append(entries, entry)
+ }
+ return entries, nil
+}
+
+// ImportKubeconfig merges a kubeconfig YAML string into the existing kubeconfig file.
+// It backs up the existing file first, then merges new contexts/clusters/users.
+// Returns lists of added and skipped context names.
+//
+// SECURITY: AuthInfo entries with Exec plugins are rejected to prevent RCE (#7260).
+func (k *KubectlProxy) ImportKubeconfig(yamlContent string) (added []string, skipped []string, err error) {
+ incoming, err := clientcmd.Load([]byte(yamlContent))
+ if err != nil {
+ return nil, nil, fmt.Errorf("invalid kubeconfig YAML: %w", err)
+ }
+ if len(incoming.Contexts) == 0 {
+ return nil, nil, fmt.Errorf("kubeconfig contains no contexts")
+ }
+
+ // SECURITY: Reject any AuthInfo with an exec plugin — uploading a
+ // kubeconfig with exec.command = "/bin/sh" achieves RCE (#7260).
+ for name, ai := range incoming.AuthInfos {
+ if ai != nil && ai.Exec != nil {
+ return nil, nil, fmt.Errorf("SECURITY: kubeconfig user %q uses exec-based auth (command: %s) — exec plugins are not allowed for imported configs", name, ai.Exec.Command)
+ }
+ }
+
+ k.mu.Lock()
+ defer k.mu.Unlock()
+
+ // Backup existing kubeconfig if the file exists.
+ // Uses UnixNano to avoid collisions from concurrent imports (#7276).
+ if _, statErr := os.Stat(k.kubeconfig); statErr == nil {
+ backupPath := fmt.Sprintf("%s.bak-%d", k.kubeconfig, time.Now().UnixNano())
+ data, readErr := os.ReadFile(k.kubeconfig)
+ if readErr != nil {
+ return nil, nil, fmt.Errorf("failed to read kubeconfig for backup: %w", readErr)
+ }
+ if writeErr := os.WriteFile(backupPath, data, 0600); writeErr != nil {
+ return nil, nil, fmt.Errorf("failed to write backup: %w", writeErr)
+ }
+ }
+
+ // Initialise maps if they are nil (empty starting config)
+ if k.config.Contexts == nil {
+ k.config.Contexts = make(map[string]*api.Context)
+ }
+ if k.config.Clusters == nil {
+ k.config.Clusters = make(map[string]*api.Cluster)
+ }
+ if k.config.AuthInfos == nil {
+ k.config.AuthInfos = make(map[string]*api.AuthInfo)
+ }
+
+ for name, ctx := range incoming.Contexts {
+ if _, exists := k.config.Contexts[name]; exists {
+ skipped = append(skipped, name)
+ continue
+ }
+
+ // Resolve cluster name collisions: if the name already exists with
+ // different data, pick a unique name so we don't silently drop the
+ // incoming cluster definition.
+ clusterName := ctx.Cluster
+ if incomingCluster, ok := incoming.Clusters[clusterName]; ok {
+ if existing, exists := k.config.Clusters[clusterName]; exists {
+ if !clustersEquivalent(existing, incomingCluster) {
+ clusterName = uniqueName(clusterName, k.config.Clusters)
+ }
+ // else: same data, reuse existing entry
+ }
+ }
+
+ // Resolve user/auth-info name collisions the same way.
+ userName := ctx.AuthInfo
+ if incomingUser, ok := incoming.AuthInfos[userName]; ok {
+ if existing, exists := k.config.AuthInfos[userName]; exists {
+ if !authInfosEquivalent(existing, incomingUser) {
+ userName = uniqueName(userName, k.config.AuthInfos)
+ }
+ }
+ }
+
+ // Build the context with possibly-renamed references.
+ mergedCtx := ctx.DeepCopy()
+ mergedCtx.Cluster = clusterName
+ mergedCtx.AuthInfo = userName
+ k.config.Contexts[name] = mergedCtx
+
+ // Add referenced cluster if present
+ if cluster, ok := incoming.Clusters[ctx.Cluster]; ok {
+ if _, exists := k.config.Clusters[clusterName]; !exists {
+ k.config.Clusters[clusterName] = cluster
+ }
+ }
+ // Add referenced user if present
+ if user, ok := incoming.AuthInfos[ctx.AuthInfo]; ok {
+ if _, exists := k.config.AuthInfos[userName]; !exists {
+ k.config.AuthInfos[userName] = user
+ }
+ }
+ added = append(added, name)
+ }
+
+ // Write merged config
+ if writeErr := clientcmd.WriteToFile(*k.config, k.kubeconfig); writeErr != nil {
+ return nil, nil, fmt.Errorf("failed to write merged kubeconfig: %w", writeErr)
+ }
+
+ // Reload from file to stay in sync (already holding lock, use internal variant)
+ k.reloadLocked()
+
+ return added, skipped, nil
+}
+
+// clustersEquivalent returns true if two Cluster structs carry the same
+// semantic configuration. The LocationOfOrigin field is ignored because it
+// reflects which file a value was loaded from, not the cluster definition.
+func clustersEquivalent(a, b *api.Cluster) bool {
+ if a == nil || b == nil {
+ return a == b
+ }
+ ac := a.DeepCopy()
+ bc := b.DeepCopy()
+ ac.LocationOfOrigin = ""
+ bc.LocationOfOrigin = ""
+ return reflect.DeepEqual(ac, bc)
+}
+
+// authInfosEquivalent is the AuthInfo analogue of clustersEquivalent.
+func authInfosEquivalent(a, b *api.AuthInfo) bool {
+ if a == nil || b == nil {
+ return a == b
+ }
+ ac := a.DeepCopy()
+ bc := b.DeepCopy()
+ ac.LocationOfOrigin = ""
+ bc.LocationOfOrigin = ""
+ return reflect.DeepEqual(ac, bc)
+}
+
+// uniqueName returns a name that does not collide with any key in m.
+// It tries "-imported", then "-imported-2", "-imported-3", etc.
+func uniqueName[V any](base string, m map[string]V) string {
+ candidate := base + "-imported"
+ if _, exists := m[candidate]; !exists {
+ return candidate
+ }
+ for i := 2; ; i++ {
+ candidate = fmt.Sprintf("%s-imported-%d", base, i)
+ if _, exists := m[candidate]; !exists {
+ return candidate
+ }
+ }
+}
+
+// AddClusterRequest describes the form fields for adding a cluster.
+type AddClusterRequest struct {
+ ContextName string `json:"contextName"`
+ ClusterName string `json:"clusterName"`
+ ServerURL string `json:"serverUrl"`
+ AuthType string `json:"authType"` // "token", "certificate"
+ Token string `json:"token,omitempty"`
+ CertData string `json:"certData,omitempty"` // base64 PEM
+ KeyData string `json:"keyData,omitempty"` // base64 PEM
+ CAData string `json:"caData,omitempty"` // base64 PEM CA cert
+ SkipTLSVerify bool `json:"skipTlsVerify,omitempty"`
+ Namespace string `json:"namespace,omitempty"` // default namespace
+}
+
+// AddCluster builds a kubeconfig entry from structured input and merges it.
+// Uses mutex for thread safety (#7259) and UnixNano for backup paths (#7276).
+func (k *KubectlProxy) AddCluster(req AddClusterRequest) error {
+ k.mu.Lock()
+ defer k.mu.Unlock()
+
+ // Validate required fields
+ if req.ContextName == "" || req.ClusterName == "" || req.ServerURL == "" || req.AuthType == "" {
+ return fmt.Errorf("contextName, clusterName, serverUrl, and authType are required")
+ }
+
+ // Validate server URL format
+ parsedURL, err := url.Parse(req.ServerURL)
+ if err != nil {
+ return fmt.Errorf("invalid server URL: %w", err)
+ }
+ if parsedURL.Scheme == "" || parsedURL.Host == "" {
+ return fmt.Errorf("server URL must include a scheme and host (e.g. https://api.example.com:6443)")
+ }
+
+ // Validate auth-type-specific fields
+ switch req.AuthType {
+ case "token":
+ if req.Token == "" {
+ return fmt.Errorf("token is required for token auth type")
+ }
+ case "certificate":
+ if req.CertData == "" || req.KeyData == "" {
+ return fmt.Errorf("certData and keyData are required for certificate auth type")
+ }
+ default:
+ return fmt.Errorf("unsupported authType: %s (must be token or certificate)", req.AuthType)
+ }
+
+ // Check context doesn't already exist
+ if k.config.Contexts != nil {
+ if _, exists := k.config.Contexts[req.ContextName]; exists {
+ return fmt.Errorf("context %q already exists", req.ContextName)
+ }
+ }
+
+ // Build cluster entry
+ cluster := &api.Cluster{
+ Server: req.ServerURL,
+ InsecureSkipTLSVerify: req.SkipTLSVerify,
+ }
+ if req.CAData != "" {
+ caBytes, err := base64.StdEncoding.DecodeString(req.CAData)
+ if err != nil {
+ return fmt.Errorf("invalid caData base64: %w", err)
+ }
+ cluster.CertificateAuthorityData = caBytes
+ }
+
+ // Build auth info entry
+ userName := req.ContextName + "-user"
+ authInfo := &api.AuthInfo{}
+ switch req.AuthType {
+ case "token":
+ authInfo.Token = req.Token
+ case "certificate":
+ certBytes, err := base64.StdEncoding.DecodeString(req.CertData)
+ if err != nil {
+ return fmt.Errorf("invalid certData base64: %w", err)
+ }
+ keyBytes, err := base64.StdEncoding.DecodeString(req.KeyData)
+ if err != nil {
+ return fmt.Errorf("invalid keyData base64: %w", err)
+ }
+ authInfo.ClientCertificateData = certBytes
+ authInfo.ClientKeyData = keyBytes
+ }
+
+ // Build context entry
+ ctx := &api.Context{
+ Cluster: req.ClusterName,
+ AuthInfo: userName,
+ Namespace: req.Namespace,
+ }
+
+ // Backup existing kubeconfig if the file exists.
+ // Uses UnixNano to avoid collisions from concurrent imports (#7276).
+ if _, statErr := os.Stat(k.kubeconfig); statErr == nil {
+ backupPath := fmt.Sprintf("%s.bak-%d", k.kubeconfig, time.Now().UnixNano())
+ data, readErr := os.ReadFile(k.kubeconfig)
+ if readErr != nil {
+ return fmt.Errorf("failed to read kubeconfig for backup: %w", readErr)
+ }
+ if writeErr := os.WriteFile(backupPath, data, 0600); writeErr != nil {
+ return fmt.Errorf("failed to write backup: %w", writeErr)
+ }
+ }
+
+ // Initialise maps if nil
+ if k.config.Contexts == nil {
+ k.config.Contexts = make(map[string]*api.Context)
+ }
+ if k.config.Clusters == nil {
+ k.config.Clusters = make(map[string]*api.Cluster)
+ }
+ if k.config.AuthInfos == nil {
+ k.config.AuthInfos = make(map[string]*api.AuthInfo)
+ }
+
+ // Add entries
+ k.config.Clusters[req.ClusterName] = cluster
+ k.config.AuthInfos[userName] = authInfo
+ k.config.Contexts[req.ContextName] = ctx
+
+ // Write to file
+ if writeErr := clientcmd.WriteToFile(*k.config, k.kubeconfig); writeErr != nil {
+ return fmt.Errorf("failed to write kubeconfig: %w", writeErr)
+ }
+
+ // Reload (already holding lock, use internal variant)
+ k.reloadLocked()
+ return nil
+}