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