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