2023-05-02 10:33:06 -05:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2020-04-13 15:17:17 -05:00
|
|
|
package kubernetes
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
2021-04-20 09:05:45 -05:00
|
|
|
"path/filepath"
|
2020-04-13 15:17:17 -05:00
|
|
|
|
|
|
|
"github.com/mitchellh/go-homedir"
|
2023-09-20 06:35:35 -05:00
|
|
|
"github.com/opentofu/opentofu/internal/backend"
|
|
|
|
"github.com/opentofu/opentofu/internal/httpclient"
|
|
|
|
"github.com/opentofu/opentofu/internal/legacy/helper/schema"
|
|
|
|
"github.com/opentofu/opentofu/version"
|
2020-04-13 15:17:17 -05:00
|
|
|
k8sSchema "k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
"k8s.io/client-go/dynamic"
|
|
|
|
"k8s.io/client-go/kubernetes"
|
|
|
|
coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1"
|
|
|
|
restclient "k8s.io/client-go/rest"
|
|
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
|
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
secretResource = k8sSchema.GroupVersionResource{
|
|
|
|
Group: "",
|
|
|
|
Version: "v1",
|
|
|
|
Resource: "secrets",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
// New creates a new backend for kubernetes remote state.
|
|
|
|
func New() backend.Backend {
|
|
|
|
s := &schema.Backend{
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
|
|
"secret_suffix": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Required: true,
|
|
|
|
Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.",
|
|
|
|
},
|
|
|
|
"labels": {
|
|
|
|
Type: schema.TypeMap,
|
|
|
|
Optional: true,
|
|
|
|
Description: "Map of additional labels to be applied to the secret.",
|
|
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
|
|
},
|
|
|
|
"namespace": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_NAMESPACE", "default"),
|
|
|
|
Description: "Namespace to store the secret in.",
|
|
|
|
},
|
|
|
|
"in_cluster_config": {
|
|
|
|
Type: schema.TypeBool,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_IN_CLUSTER_CONFIG", false),
|
|
|
|
Description: "Used to authenticate to the cluster from inside a pod.",
|
|
|
|
},
|
|
|
|
"load_config_file": {
|
|
|
|
Type: schema.TypeBool,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_LOAD_CONFIG_FILE", true),
|
|
|
|
Description: "Load local kubeconfig.",
|
|
|
|
},
|
|
|
|
"host": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""),
|
|
|
|
Description: "The hostname (in form of URI) of Kubernetes master.",
|
|
|
|
},
|
|
|
|
"username": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", ""),
|
|
|
|
Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
|
|
|
|
},
|
|
|
|
"password": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", ""),
|
|
|
|
Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
|
|
|
|
},
|
|
|
|
"insecure": {
|
|
|
|
Type: schema.TypeBool,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false),
|
|
|
|
Description: "Whether server should be accessed without verifying the TLS certificate.",
|
|
|
|
},
|
|
|
|
"client_certificate": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""),
|
|
|
|
Description: "PEM-encoded client certificate for TLS authentication.",
|
|
|
|
},
|
|
|
|
"client_key": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""),
|
|
|
|
Description: "PEM-encoded client certificate key for TLS authentication.",
|
|
|
|
},
|
|
|
|
"cluster_ca_certificate": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""),
|
|
|
|
Description: "PEM-encoded root certificates bundle for TLS authentication.",
|
|
|
|
},
|
2021-04-20 09:05:45 -05:00
|
|
|
"config_paths": {
|
|
|
|
Type: schema.TypeList,
|
|
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
|
|
Optional: true,
|
|
|
|
Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.",
|
|
|
|
},
|
2020-04-13 15:17:17 -05:00
|
|
|
"config_path": {
|
2021-04-20 09:05:45 -05:00
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", ""),
|
|
|
|
Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.",
|
2020-04-13 15:17:17 -05:00
|
|
|
},
|
|
|
|
"config_context": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""),
|
|
|
|
},
|
|
|
|
"config_context_auth_info": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""),
|
|
|
|
Description: "",
|
|
|
|
},
|
|
|
|
"config_context_cluster": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""),
|
|
|
|
Description: "",
|
|
|
|
},
|
|
|
|
"token": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Optional: true,
|
|
|
|
DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""),
|
|
|
|
Description: "Token to authentifcate a service account.",
|
|
|
|
},
|
|
|
|
"exec": {
|
|
|
|
Type: schema.TypeList,
|
|
|
|
Optional: true,
|
|
|
|
MaxItems: 1,
|
|
|
|
Elem: &schema.Resource{
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
|
|
"api_version": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
"command": {
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
"env": {
|
|
|
|
Type: schema.TypeMap,
|
|
|
|
Optional: true,
|
|
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
|
|
},
|
|
|
|
"args": {
|
|
|
|
Type: schema.TypeList,
|
|
|
|
Optional: true,
|
|
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Description: "Use a credential plugin to authenticate.",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
result := &Backend{Backend: s}
|
|
|
|
result.Backend.ConfigureFunc = result.configure
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
type Backend struct {
|
|
|
|
*schema.Backend
|
|
|
|
|
|
|
|
// The fields below are set from configure
|
|
|
|
kubernetesSecretClient dynamic.ResourceInterface
|
|
|
|
kubernetesLeaseClient coordinationv1.LeaseInterface
|
|
|
|
config *restclient.Config
|
|
|
|
namespace string
|
|
|
|
labels map[string]string
|
|
|
|
nameSuffix string
|
|
|
|
}
|
|
|
|
|
2023-10-04 04:16:27 -05:00
|
|
|
func (b Backend) getKubernetesSecretClient() (dynamic.ResourceInterface, error) {
|
2020-04-13 15:17:17 -05:00
|
|
|
if b.kubernetesSecretClient != nil {
|
|
|
|
return b.kubernetesSecretClient, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
client, err := dynamic.NewForConfig(b.config)
|
|
|
|
if err != nil {
|
2023-09-14 05:57:12 -05:00
|
|
|
return nil, fmt.Errorf("Failed to configure: %w", err)
|
2020-04-13 15:17:17 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
b.kubernetesSecretClient = client.Resource(secretResource).Namespace(b.namespace)
|
|
|
|
return b.kubernetesSecretClient, nil
|
|
|
|
}
|
|
|
|
|
2023-10-04 04:16:27 -05:00
|
|
|
func (b Backend) getKubernetesLeaseClient() (coordinationv1.LeaseInterface, error) {
|
2020-04-13 15:17:17 -05:00
|
|
|
if b.kubernetesLeaseClient != nil {
|
|
|
|
return b.kubernetesLeaseClient, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
client, err := kubernetes.NewForConfig(b.config)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
b.kubernetesLeaseClient = client.CoordinationV1().Leases(b.namespace)
|
|
|
|
return b.kubernetesLeaseClient, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Backend) configure(ctx context.Context) error {
|
|
|
|
if b.config != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Grab the resource data
|
|
|
|
data := schema.FromContextBackendConfig(ctx)
|
|
|
|
|
|
|
|
cfg, err := getInitialConfig(data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Overriding with static configuration
|
2023-09-20 06:59:20 -05:00
|
|
|
cfg.UserAgent = httpclient.OpenTofuUserAgent(version.Version)
|
2020-04-13 15:17:17 -05:00
|
|
|
|
|
|
|
if v, ok := data.GetOk("host"); ok {
|
|
|
|
cfg.Host = v.(string)
|
|
|
|
}
|
|
|
|
if v, ok := data.GetOk("username"); ok {
|
|
|
|
cfg.Username = v.(string)
|
|
|
|
}
|
|
|
|
if v, ok := data.GetOk("password"); ok {
|
|
|
|
cfg.Password = v.(string)
|
|
|
|
}
|
|
|
|
if v, ok := data.GetOk("insecure"); ok {
|
|
|
|
cfg.Insecure = v.(bool)
|
|
|
|
}
|
|
|
|
if v, ok := data.GetOk("cluster_ca_certificate"); ok {
|
|
|
|
cfg.CAData = bytes.NewBufferString(v.(string)).Bytes()
|
|
|
|
}
|
|
|
|
if v, ok := data.GetOk("client_certificate"); ok {
|
|
|
|
cfg.CertData = bytes.NewBufferString(v.(string)).Bytes()
|
|
|
|
}
|
|
|
|
if v, ok := data.GetOk("client_key"); ok {
|
|
|
|
cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes()
|
|
|
|
}
|
|
|
|
if v, ok := data.GetOk("token"); ok {
|
|
|
|
cfg.BearerToken = v.(string)
|
|
|
|
}
|
|
|
|
|
|
|
|
if v, ok := data.GetOk("labels"); ok {
|
|
|
|
labels := map[string]string{}
|
|
|
|
for k, vv := range v.(map[string]interface{}) {
|
|
|
|
labels[k] = vv.(string)
|
|
|
|
}
|
|
|
|
b.labels = labels
|
|
|
|
}
|
|
|
|
|
|
|
|
ns := data.Get("namespace").(string)
|
|
|
|
b.namespace = ns
|
|
|
|
b.nameSuffix = data.Get("secret_suffix").(string)
|
|
|
|
b.config = cfg
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) {
|
|
|
|
var cfg *restclient.Config
|
|
|
|
var err error
|
|
|
|
|
|
|
|
inCluster := data.Get("in_cluster_config").(bool)
|
|
|
|
if inCluster {
|
|
|
|
cfg, err = restclient.InClusterConfig()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
cfg, err = tryLoadingConfigFile(data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg == nil {
|
|
|
|
cfg = &restclient.Config{}
|
|
|
|
}
|
|
|
|
return cfg, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) {
|
2021-04-20 09:05:45 -05:00
|
|
|
loader := &clientcmd.ClientConfigLoadingRules{}
|
|
|
|
|
|
|
|
configPaths := []string{}
|
|
|
|
if v, ok := d.Get("config_path").(string); ok && v != "" {
|
|
|
|
configPaths = []string{v}
|
|
|
|
} else if v, ok := d.Get("config_paths").([]interface{}); ok && len(v) > 0 {
|
|
|
|
for _, p := range v {
|
|
|
|
configPaths = append(configPaths, p.(string))
|
|
|
|
}
|
|
|
|
} else if v := os.Getenv("KUBE_CONFIG_PATHS"); v != "" {
|
|
|
|
configPaths = filepath.SplitList(v)
|
|
|
|
}
|
|
|
|
|
|
|
|
expandedPaths := []string{}
|
|
|
|
for _, p := range configPaths {
|
|
|
|
path, err := homedir.Expand(p)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[DEBUG] Could not expand path: %s", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
log.Printf("[DEBUG] Using kubeconfig: %s", path)
|
|
|
|
expandedPaths = append(expandedPaths, path)
|
2020-04-13 15:17:17 -05:00
|
|
|
}
|
|
|
|
|
2021-04-20 09:05:45 -05:00
|
|
|
if len(expandedPaths) == 1 {
|
|
|
|
loader.ExplicitPath = expandedPaths[0]
|
|
|
|
} else {
|
|
|
|
loader.Precedence = expandedPaths
|
2020-04-13 15:17:17 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
overrides := &clientcmd.ConfigOverrides{}
|
|
|
|
ctxSuffix := "; default context"
|
|
|
|
|
|
|
|
ctx, ctxOk := d.GetOk("config_context")
|
|
|
|
authInfo, authInfoOk := d.GetOk("config_context_auth_info")
|
|
|
|
cluster, clusterOk := d.GetOk("config_context_cluster")
|
|
|
|
if ctxOk || authInfoOk || clusterOk {
|
|
|
|
ctxSuffix = "; overriden context"
|
|
|
|
if ctxOk {
|
|
|
|
overrides.CurrentContext = ctx.(string)
|
|
|
|
ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext)
|
|
|
|
log.Printf("[DEBUG] Using custom current context: %q", overrides.CurrentContext)
|
|
|
|
}
|
|
|
|
|
|
|
|
overrides.Context = clientcmdapi.Context{}
|
|
|
|
if authInfoOk {
|
|
|
|
overrides.Context.AuthInfo = authInfo.(string)
|
|
|
|
ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo)
|
|
|
|
}
|
|
|
|
if clusterOk {
|
|
|
|
overrides.Context.Cluster = cluster.(string)
|
|
|
|
ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster)
|
|
|
|
}
|
|
|
|
log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context)
|
|
|
|
}
|
|
|
|
|
|
|
|
if v, ok := d.GetOk("exec"); ok {
|
|
|
|
exec := &clientcmdapi.ExecConfig{}
|
|
|
|
if spec, ok := v.([]interface{})[0].(map[string]interface{}); ok {
|
|
|
|
exec.APIVersion = spec["api_version"].(string)
|
|
|
|
exec.Command = spec["command"].(string)
|
|
|
|
exec.Args = expandStringSlice(spec["args"].([]interface{}))
|
|
|
|
for kk, vv := range spec["env"].(map[string]interface{}) {
|
|
|
|
exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{Name: kk, Value: vv.(string)})
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return nil, fmt.Errorf("Failed to parse exec")
|
|
|
|
}
|
|
|
|
overrides.AuthInfo.Exec = exec
|
|
|
|
}
|
|
|
|
|
|
|
|
cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides)
|
|
|
|
cfg, err := cc.ClientConfig()
|
|
|
|
if err != nil {
|
|
|
|
if pathErr, ok := err.(*os.PathError); ok && os.IsNotExist(pathErr.Err) {
|
2021-04-20 09:05:45 -05:00
|
|
|
log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", pathErr.Path)
|
2020-04-13 15:17:17 -05:00
|
|
|
return nil, nil
|
|
|
|
}
|
2023-09-14 05:57:12 -05:00
|
|
|
return nil, fmt.Errorf("Failed to initialize kubernetes configuration: %w", err)
|
2020-04-13 15:17:17 -05:00
|
|
|
}
|
|
|
|
|
2021-04-20 09:05:45 -05:00
|
|
|
log.Printf("[INFO] Successfully initialized config")
|
2020-04-13 15:17:17 -05:00
|
|
|
return cfg, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func expandStringSlice(s []interface{}) []string {
|
|
|
|
result := make([]string, len(s), len(s))
|
|
|
|
for k, v := range s {
|
|
|
|
// Handle the Terraform parser bug which turns empty strings in lists to nil.
|
|
|
|
if v == nil {
|
|
|
|
result[k] = ""
|
|
|
|
} else {
|
|
|
|
result[k] = v.(string)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|