github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/kubernetes/backend.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package kubernetes 7 8 import ( 9 "bytes" 10 "context" 11 "fmt" 12 "log" 13 "os" 14 "path/filepath" 15 16 "github.com/mitchellh/go-homedir" 17 "github.com/opentofu/opentofu/internal/backend" 18 "github.com/opentofu/opentofu/internal/encryption" 19 "github.com/opentofu/opentofu/internal/httpclient" 20 "github.com/opentofu/opentofu/internal/legacy/helper/schema" 21 "github.com/opentofu/opentofu/version" 22 k8sSchema "k8s.io/apimachinery/pkg/runtime/schema" 23 "k8s.io/client-go/dynamic" 24 "k8s.io/client-go/kubernetes" 25 coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1" 26 restclient "k8s.io/client-go/rest" 27 "k8s.io/client-go/tools/clientcmd" 28 clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 29 ) 30 31 var ( 32 secretResource = k8sSchema.GroupVersionResource{ 33 Group: "", 34 Version: "v1", 35 Resource: "secrets", 36 } 37 ) 38 39 // New creates a new backend for kubernetes remote state. 40 func New(enc encryption.StateEncryption) backend.Backend { 41 s := &schema.Backend{ 42 Schema: map[string]*schema.Schema{ 43 "secret_suffix": { 44 Type: schema.TypeString, 45 Required: true, 46 Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.", 47 }, 48 "labels": { 49 Type: schema.TypeMap, 50 Optional: true, 51 Description: "Map of additional labels to be applied to the secret.", 52 Elem: &schema.Schema{Type: schema.TypeString}, 53 }, 54 "namespace": { 55 Type: schema.TypeString, 56 Optional: true, 57 DefaultFunc: schema.EnvDefaultFunc("KUBE_NAMESPACE", "default"), 58 Description: "Namespace to store the secret in.", 59 }, 60 "in_cluster_config": { 61 Type: schema.TypeBool, 62 Optional: true, 63 DefaultFunc: schema.EnvDefaultFunc("KUBE_IN_CLUSTER_CONFIG", false), 64 Description: "Used to authenticate to the cluster from inside a pod.", 65 }, 66 "load_config_file": { 67 Type: schema.TypeBool, 68 Optional: true, 69 DefaultFunc: schema.EnvDefaultFunc("KUBE_LOAD_CONFIG_FILE", true), 70 Description: "Load local kubeconfig.", 71 }, 72 "host": { 73 Type: schema.TypeString, 74 Optional: true, 75 DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""), 76 Description: "The hostname (in form of URI) of Kubernetes master.", 77 }, 78 "username": { 79 Type: schema.TypeString, 80 Optional: true, 81 DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", ""), 82 Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", 83 }, 84 "password": { 85 Type: schema.TypeString, 86 Optional: true, 87 DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", ""), 88 Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", 89 }, 90 "insecure": { 91 Type: schema.TypeBool, 92 Optional: true, 93 DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false), 94 Description: "Whether server should be accessed without verifying the TLS certificate.", 95 }, 96 "client_certificate": { 97 Type: schema.TypeString, 98 Optional: true, 99 DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""), 100 Description: "PEM-encoded client certificate for TLS authentication.", 101 }, 102 "client_key": { 103 Type: schema.TypeString, 104 Optional: true, 105 DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""), 106 Description: "PEM-encoded client certificate key for TLS authentication.", 107 }, 108 "cluster_ca_certificate": { 109 Type: schema.TypeString, 110 Optional: true, 111 DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""), 112 Description: "PEM-encoded root certificates bundle for TLS authentication.", 113 }, 114 "config_paths": { 115 Type: schema.TypeList, 116 Elem: &schema.Schema{Type: schema.TypeString}, 117 Optional: true, 118 Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.", 119 }, 120 "config_path": { 121 Type: schema.TypeString, 122 Optional: true, 123 DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", ""), 124 Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.", 125 }, 126 "config_context": { 127 Type: schema.TypeString, 128 Optional: true, 129 DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""), 130 }, 131 "config_context_auth_info": { 132 Type: schema.TypeString, 133 Optional: true, 134 DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""), 135 Description: "", 136 }, 137 "config_context_cluster": { 138 Type: schema.TypeString, 139 Optional: true, 140 DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""), 141 Description: "", 142 }, 143 "token": { 144 Type: schema.TypeString, 145 Optional: true, 146 DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""), 147 Description: "Token to authentifcate a service account.", 148 }, 149 "exec": { 150 Type: schema.TypeList, 151 Optional: true, 152 MaxItems: 1, 153 Elem: &schema.Resource{ 154 Schema: map[string]*schema.Schema{ 155 "api_version": { 156 Type: schema.TypeString, 157 Required: true, 158 }, 159 "command": { 160 Type: schema.TypeString, 161 Required: true, 162 }, 163 "env": { 164 Type: schema.TypeMap, 165 Optional: true, 166 Elem: &schema.Schema{Type: schema.TypeString}, 167 }, 168 "args": { 169 Type: schema.TypeList, 170 Optional: true, 171 Elem: &schema.Schema{Type: schema.TypeString}, 172 }, 173 }, 174 }, 175 Description: "Use a credential plugin to authenticate.", 176 }, 177 }, 178 } 179 180 result := &Backend{Backend: s, encryption: enc} 181 result.Backend.ConfigureFunc = result.configure 182 return result 183 } 184 185 type Backend struct { 186 *schema.Backend 187 encryption encryption.StateEncryption 188 189 // The fields below are set from configure 190 kubernetesSecretClient dynamic.ResourceInterface 191 kubernetesLeaseClient coordinationv1.LeaseInterface 192 config *restclient.Config 193 namespace string 194 labels map[string]string 195 nameSuffix string 196 } 197 198 func (b Backend) getKubernetesSecretClient() (dynamic.ResourceInterface, error) { 199 if b.kubernetesSecretClient != nil { 200 return b.kubernetesSecretClient, nil 201 } 202 203 client, err := dynamic.NewForConfig(b.config) 204 if err != nil { 205 return nil, fmt.Errorf("Failed to configure: %w", err) 206 } 207 208 b.kubernetesSecretClient = client.Resource(secretResource).Namespace(b.namespace) 209 return b.kubernetesSecretClient, nil 210 } 211 212 func (b Backend) getKubernetesLeaseClient() (coordinationv1.LeaseInterface, error) { 213 if b.kubernetesLeaseClient != nil { 214 return b.kubernetesLeaseClient, nil 215 } 216 217 client, err := kubernetes.NewForConfig(b.config) 218 if err != nil { 219 return nil, err 220 } 221 222 b.kubernetesLeaseClient = client.CoordinationV1().Leases(b.namespace) 223 return b.kubernetesLeaseClient, nil 224 } 225 226 func (b *Backend) configure(ctx context.Context) error { 227 if b.config != nil { 228 return nil 229 } 230 231 // Grab the resource data 232 data := schema.FromContextBackendConfig(ctx) 233 234 cfg, err := getInitialConfig(data) 235 if err != nil { 236 return err 237 } 238 239 // Overriding with static configuration 240 cfg.UserAgent = httpclient.OpenTofuUserAgent(version.Version) 241 242 if v, ok := data.GetOk("host"); ok { 243 cfg.Host = v.(string) 244 } 245 if v, ok := data.GetOk("username"); ok { 246 cfg.Username = v.(string) 247 } 248 if v, ok := data.GetOk("password"); ok { 249 cfg.Password = v.(string) 250 } 251 if v, ok := data.GetOk("insecure"); ok { 252 cfg.Insecure = v.(bool) 253 } 254 if v, ok := data.GetOk("cluster_ca_certificate"); ok { 255 cfg.CAData = bytes.NewBufferString(v.(string)).Bytes() 256 } 257 if v, ok := data.GetOk("client_certificate"); ok { 258 cfg.CertData = bytes.NewBufferString(v.(string)).Bytes() 259 } 260 if v, ok := data.GetOk("client_key"); ok { 261 cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes() 262 } 263 if v, ok := data.GetOk("token"); ok { 264 cfg.BearerToken = v.(string) 265 } 266 267 if v, ok := data.GetOk("labels"); ok { 268 labels := map[string]string{} 269 for k, vv := range v.(map[string]interface{}) { 270 labels[k] = vv.(string) 271 } 272 b.labels = labels 273 } 274 275 ns := data.Get("namespace").(string) 276 b.namespace = ns 277 b.nameSuffix = data.Get("secret_suffix").(string) 278 b.config = cfg 279 280 return nil 281 } 282 283 func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) { 284 var cfg *restclient.Config 285 var err error 286 287 inCluster := data.Get("in_cluster_config").(bool) 288 if inCluster { 289 cfg, err = restclient.InClusterConfig() 290 if err != nil { 291 return nil, err 292 } 293 } else { 294 cfg, err = tryLoadingConfigFile(data) 295 if err != nil { 296 return nil, err 297 } 298 } 299 300 if cfg == nil { 301 cfg = &restclient.Config{} 302 } 303 return cfg, err 304 } 305 306 func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { 307 loader := &clientcmd.ClientConfigLoadingRules{} 308 309 configPaths := []string{} 310 if v, ok := d.Get("config_path").(string); ok && v != "" { 311 configPaths = []string{v} 312 } else if v, ok := d.Get("config_paths").([]interface{}); ok && len(v) > 0 { 313 for _, p := range v { 314 configPaths = append(configPaths, p.(string)) 315 } 316 } else if v := os.Getenv("KUBE_CONFIG_PATHS"); v != "" { 317 configPaths = filepath.SplitList(v) 318 } 319 320 expandedPaths := []string{} 321 for _, p := range configPaths { 322 path, err := homedir.Expand(p) 323 if err != nil { 324 log.Printf("[DEBUG] Could not expand path: %s", err) 325 return nil, err 326 } 327 log.Printf("[DEBUG] Using kubeconfig: %s", path) 328 expandedPaths = append(expandedPaths, path) 329 } 330 331 if len(expandedPaths) == 1 { 332 loader.ExplicitPath = expandedPaths[0] 333 } else { 334 loader.Precedence = expandedPaths 335 } 336 337 overrides := &clientcmd.ConfigOverrides{} 338 ctxSuffix := "; default context" 339 340 ctx, ctxOk := d.GetOk("config_context") 341 authInfo, authInfoOk := d.GetOk("config_context_auth_info") 342 cluster, clusterOk := d.GetOk("config_context_cluster") 343 if ctxOk || authInfoOk || clusterOk { 344 ctxSuffix = "; overriden context" 345 if ctxOk { 346 overrides.CurrentContext = ctx.(string) 347 ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext) 348 log.Printf("[DEBUG] Using custom current context: %q", overrides.CurrentContext) 349 } 350 351 overrides.Context = clientcmdapi.Context{} 352 if authInfoOk { 353 overrides.Context.AuthInfo = authInfo.(string) 354 ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo) 355 } 356 if clusterOk { 357 overrides.Context.Cluster = cluster.(string) 358 ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster) 359 } 360 log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context) 361 } 362 363 if v, ok := d.GetOk("exec"); ok { 364 exec := &clientcmdapi.ExecConfig{} 365 if spec, ok := v.([]interface{})[0].(map[string]interface{}); ok { 366 exec.APIVersion = spec["api_version"].(string) 367 exec.Command = spec["command"].(string) 368 exec.Args = expandStringSlice(spec["args"].([]interface{})) 369 for kk, vv := range spec["env"].(map[string]interface{}) { 370 exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{Name: kk, Value: vv.(string)}) 371 } 372 } else { 373 return nil, fmt.Errorf("Failed to parse exec") 374 } 375 overrides.AuthInfo.Exec = exec 376 } 377 378 cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) 379 cfg, err := cc.ClientConfig() 380 if err != nil { 381 if pathErr, ok := err.(*os.PathError); ok && os.IsNotExist(pathErr.Err) { 382 log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", pathErr.Path) 383 return nil, nil 384 } 385 return nil, fmt.Errorf("Failed to initialize kubernetes configuration: %w", err) 386 } 387 388 log.Printf("[INFO] Successfully initialized config") 389 return cfg, nil 390 } 391 392 func expandStringSlice(s []interface{}) []string { 393 result := make([]string, len(s), len(s)) 394 for k, v := range s { 395 // Handle the OpenTofu parser bug which turns empty strings in lists to nil. 396 if v == nil { 397 result[k] = "" 398 } else { 399 result[k] = v.(string) 400 } 401 } 402 return result 403 }