github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/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 10 "github.com/iaas-resource-provision/iaas-rpc/internal/backend" 11 "github.com/iaas-resource-provision/iaas-rpc/internal/legacy/helper/schema" 12 "github.com/iaas-resource-provision/iaas-rpc/version" 13 "github.com/mitchellh/cli" 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_path": { 118 Type: schema.TypeString, 119 Optional: true, 120 DefaultFunc: schema.MultiEnvDefaultFunc( 121 []string{ 122 "KUBE_CONFIG", 123 "KUBECONFIG", 124 }, 125 "~/.kube/config"), 126 Description: "Path to the kube config file, defaults to ~/.kube/config", 127 }, 128 "config_context": { 129 Type: schema.TypeString, 130 Optional: true, 131 DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""), 132 }, 133 "config_context_auth_info": { 134 Type: schema.TypeString, 135 Optional: true, 136 DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""), 137 Description: "", 138 }, 139 "config_context_cluster": { 140 Type: schema.TypeString, 141 Optional: true, 142 DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""), 143 Description: "", 144 }, 145 "token": { 146 Type: schema.TypeString, 147 Optional: true, 148 DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""), 149 Description: "Token to authentifcate a service account.", 150 }, 151 "exec": { 152 Type: schema.TypeList, 153 Optional: true, 154 MaxItems: 1, 155 Elem: &schema.Resource{ 156 Schema: map[string]*schema.Schema{ 157 "api_version": { 158 Type: schema.TypeString, 159 Required: true, 160 }, 161 "command": { 162 Type: schema.TypeString, 163 Required: true, 164 }, 165 "env": { 166 Type: schema.TypeMap, 167 Optional: true, 168 Elem: &schema.Schema{Type: schema.TypeString}, 169 }, 170 "args": { 171 Type: schema.TypeList, 172 Optional: true, 173 Elem: &schema.Schema{Type: schema.TypeString}, 174 }, 175 }, 176 }, 177 Description: "Use a credential plugin to authenticate.", 178 }, 179 }, 180 } 181 182 result := &Backend{Backend: s} 183 result.Backend.ConfigureFunc = result.configure 184 return result 185 } 186 187 type Backend struct { 188 *schema.Backend 189 190 // The fields below are set from configure 191 kubernetesSecretClient dynamic.ResourceInterface 192 kubernetesLeaseClient coordinationv1.LeaseInterface 193 config *restclient.Config 194 namespace string 195 labels map[string]string 196 nameSuffix string 197 } 198 199 func (b Backend) KubernetesSecretClient() (dynamic.ResourceInterface, error) { 200 if b.kubernetesSecretClient != nil { 201 return b.kubernetesSecretClient, nil 202 } 203 204 client, err := dynamic.NewForConfig(b.config) 205 if err != nil { 206 return nil, fmt.Errorf("Failed to configure: %s", err) 207 } 208 209 b.kubernetesSecretClient = client.Resource(secretResource).Namespace(b.namespace) 210 return b.kubernetesSecretClient, nil 211 } 212 213 func (b Backend) KubernetesLeaseClient() (coordinationv1.LeaseInterface, error) { 214 if b.kubernetesLeaseClient != nil { 215 return b.kubernetesLeaseClient, nil 216 } 217 218 client, err := kubernetes.NewForConfig(b.config) 219 if err != nil { 220 return nil, err 221 } 222 223 b.kubernetesLeaseClient = client.CoordinationV1().Leases(b.namespace) 224 return b.kubernetesLeaseClient, nil 225 } 226 227 func (b *Backend) configure(ctx context.Context) error { 228 if b.config != nil { 229 return nil 230 } 231 232 // Grab the resource data 233 data := schema.FromContextBackendConfig(ctx) 234 235 cfg, err := getInitialConfig(data) 236 if err != nil { 237 return err 238 } 239 240 // Overriding with static configuration 241 cfg.UserAgent = fmt.Sprintf("HashiCorp/1.0 Terraform/%s", version.String()) 242 243 if v, ok := data.GetOk("host"); ok { 244 cfg.Host = v.(string) 245 } 246 if v, ok := data.GetOk("username"); ok { 247 cfg.Username = v.(string) 248 } 249 if v, ok := data.GetOk("password"); ok { 250 cfg.Password = v.(string) 251 } 252 if v, ok := data.GetOk("insecure"); ok { 253 cfg.Insecure = v.(bool) 254 } 255 if v, ok := data.GetOk("cluster_ca_certificate"); ok { 256 cfg.CAData = bytes.NewBufferString(v.(string)).Bytes() 257 } 258 if v, ok := data.GetOk("client_certificate"); ok { 259 cfg.CertData = bytes.NewBufferString(v.(string)).Bytes() 260 } 261 if v, ok := data.GetOk("client_key"); ok { 262 cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes() 263 } 264 if v, ok := data.GetOk("token"); ok { 265 cfg.BearerToken = v.(string) 266 } 267 268 if v, ok := data.GetOk("labels"); ok { 269 labels := map[string]string{} 270 for k, vv := range v.(map[string]interface{}) { 271 labels[k] = vv.(string) 272 } 273 b.labels = labels 274 } 275 276 ns := data.Get("namespace").(string) 277 b.namespace = ns 278 b.nameSuffix = data.Get("secret_suffix").(string) 279 b.config = cfg 280 281 return nil 282 } 283 284 func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) { 285 var cfg *restclient.Config 286 var err error 287 288 c := &cli.BasicUi{Writer: os.Stdout} 289 290 inCluster := data.Get("in_cluster_config").(bool) 291 cf := data.Get("load_config_file").(bool) 292 293 if !inCluster && !cf { 294 c.Output(noConfigError) 295 } 296 297 if inCluster { 298 cfg, err = restclient.InClusterConfig() 299 if err != nil { 300 return nil, err 301 } 302 } else { 303 cfg, err = tryLoadingConfigFile(data) 304 if err != nil { 305 return nil, err 306 } 307 } 308 309 if cfg == nil { 310 cfg = &restclient.Config{} 311 } 312 return cfg, err 313 } 314 315 func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { 316 path, err := homedir.Expand(d.Get("config_path").(string)) 317 if err != nil { 318 return nil, err 319 } 320 321 loader := &clientcmd.ClientConfigLoadingRules{ 322 ExplicitPath: path, 323 } 324 325 overrides := &clientcmd.ConfigOverrides{} 326 ctxSuffix := "; default context" 327 328 ctx, ctxOk := d.GetOk("config_context") 329 authInfo, authInfoOk := d.GetOk("config_context_auth_info") 330 cluster, clusterOk := d.GetOk("config_context_cluster") 331 if ctxOk || authInfoOk || clusterOk { 332 ctxSuffix = "; overriden context" 333 if ctxOk { 334 overrides.CurrentContext = ctx.(string) 335 ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext) 336 log.Printf("[DEBUG] Using custom current context: %q", overrides.CurrentContext) 337 } 338 339 overrides.Context = clientcmdapi.Context{} 340 if authInfoOk { 341 overrides.Context.AuthInfo = authInfo.(string) 342 ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo) 343 } 344 if clusterOk { 345 overrides.Context.Cluster = cluster.(string) 346 ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster) 347 } 348 log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context) 349 } 350 351 if v, ok := d.GetOk("exec"); ok { 352 exec := &clientcmdapi.ExecConfig{} 353 if spec, ok := v.([]interface{})[0].(map[string]interface{}); ok { 354 exec.APIVersion = spec["api_version"].(string) 355 exec.Command = spec["command"].(string) 356 exec.Args = expandStringSlice(spec["args"].([]interface{})) 357 for kk, vv := range spec["env"].(map[string]interface{}) { 358 exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{Name: kk, Value: vv.(string)}) 359 } 360 } else { 361 return nil, fmt.Errorf("Failed to parse exec") 362 } 363 overrides.AuthInfo.Exec = exec 364 } 365 366 cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) 367 cfg, err := cc.ClientConfig() 368 if err != nil { 369 if pathErr, ok := err.(*os.PathError); ok && os.IsNotExist(pathErr.Err) { 370 log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", path) 371 return nil, nil 372 } 373 return nil, fmt.Errorf("Failed to load config (%s%s): %s", path, ctxSuffix, err) 374 } 375 376 log.Printf("[INFO] Successfully loaded config file (%s%s)", path, ctxSuffix) 377 return cfg, nil 378 } 379 380 func expandStringSlice(s []interface{}) []string { 381 result := make([]string, len(s), len(s)) 382 for k, v := range s { 383 // Handle the Terraform parser bug which turns empty strings in lists to nil. 384 if v == nil { 385 result[k] = "" 386 } else { 387 result[k] = v.(string) 388 } 389 } 390 return result 391 }