diff --git a/cloudstack/data_source_cloudstack_kubernetes_cluster_config.go b/cloudstack/data_source_cloudstack_kubernetes_cluster_config.go new file mode 100644 index 00000000..3116acab --- /dev/null +++ b/cloudstack/data_source_cloudstack_kubernetes_cluster_config.go @@ -0,0 +1,283 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "encoding/base64" + "fmt" + "log" + "slices" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "gopkg.in/yaml.v3" +) + +func dataSourceCloudstackKubernetesClusterConfig() *schema.Resource { + return &schema.Resource{ + Read: datasourceCloudStackKubernetesClusterConfigRead, + Schema: map[string]*schema.Schema{ + "cluster_id": { + Type: schema.TypeString, + Required: true, + Description: "The ID of the Kubernetes cluster to retrieve the config for.", + }, + + //Computed values + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The name of the Kubernetes cluster.", + }, + + "config_data": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "The raw kubeconfig of the Kubernetes cluster.", + }, + + "endpoint": { + Type: schema.TypeString, + Computed: true, + Description: "The URL of the Kubernetes API server, taken from the kubeconfig.", + }, + + "cluster_ca_certificate": { + Type: schema.TypeString, + Computed: true, + Description: "The PEM encoded certificate authority of the Kubernetes API server.", + }, + + "client_certificate": { + Type: schema.TypeString, + Computed: true, + Description: "The PEM encoded client certificate used to authenticate against the Kubernetes API server.", + }, + + "client_key": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "The PEM encoded client key used to authenticate against the Kubernetes API server.", + }, + }, + } +} + +// kubeConfig models only the subset of a kubeconfig document that this data +// source exposes as separate attributes. +type kubeConfig struct { + CurrentContext string `yaml:"current-context"` + Clusters []kubeConfigCluster `yaml:"clusters"` + Contexts []kubeConfigContext `yaml:"contexts"` + Users []kubeConfigUser `yaml:"users"` +} + +type kubeConfigCluster struct { + Name string `yaml:"name"` + Cluster kubeConfigClusterInfo `yaml:"cluster"` +} + +type kubeConfigClusterInfo struct { + Server string `yaml:"server"` + CertificateAuthorityData string `yaml:"certificate-authority-data"` +} + +type kubeConfigContext struct { + Name string `yaml:"name"` + Context kubeConfigContextInfo `yaml:"context"` +} + +type kubeConfigContextInfo struct { + Cluster string `yaml:"cluster"` + User string `yaml:"user"` +} + +type kubeConfigUser struct { + Name string `yaml:"name"` + User kubeConfigUserInfo `yaml:"user"` +} + +type kubeConfigUserInfo struct { + ClientCertificateData string `yaml:"client-certificate-data"` + ClientKeyData string `yaml:"client-key-data"` +} + +// kubernetesClusterCredentials holds the values extracted from a kubeconfig that +// are needed to connect to the Kubernetes API server. +type kubernetesClusterCredentials struct { + Endpoint string + ClusterCACertificate string + ClientCertificate string + ClientKey string +} + +func datasourceCloudStackKubernetesClusterConfigRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + clusterID := d.Get("cluster_id").(string) + + log.Printf("[DEBUG] Retrieving config of Kubernetes Cluster %s", clusterID) + + p := cs.Kubernetes.NewGetKubernetesClusterConfigParams() + p.SetId(clusterID) + + config, err := cs.Kubernetes.GetKubernetesClusterConfig(p) + if err != nil { + // CloudStack refuses to hand out a config while the cluster is still + // starting, and when the Kubernetes service plugin is disabled. + return fmt.Errorf("Failed to get the config of Kubernetes Cluster %s: %s", clusterID, err) + } + + if config.Configdata == "" { + return fmt.Errorf("Kubernetes Cluster %s returned an empty config; the cluster ID may not "+ + "exist, the cluster may still be starting, or the Kubernetes service plugin may be disabled", clusterID) + } + + credentials, err := parseKubernetesClusterConfig(config.Configdata) + if err != nil { + return fmt.Errorf("Failed to parse the config of Kubernetes Cluster %s: %s", clusterID, err) + } + + if *credentials == (kubernetesClusterCredentials{}) { + log.Printf("[WARN] Could not derive a cluster endpoint, CA certificate, client certificate or "+ + "client key from the config of Kubernetes Cluster %s; use config_data directly instead", clusterID) + } + + d.SetId(config.Id) + d.Set("name", config.Name) + d.Set("config_data", config.Configdata) + d.Set("endpoint", credentials.Endpoint) + d.Set("cluster_ca_certificate", credentials.ClusterCACertificate) + d.Set("client_certificate", credentials.ClientCertificate) + d.Set("client_key", credentials.ClientKey) + + return nil +} + +// parseKubernetesClusterConfig extracts the endpoint and client credentials +// from a kubeconfig document. +func parseKubernetesClusterConfig(configData string) (*kubernetesClusterCredentials, error) { + var config kubeConfig + if err := yaml.Unmarshal([]byte(configData), &config); err != nil { + return nil, fmt.Errorf("Invalid kubeconfig: %s", err) + } + + // The current context is resolved the same way as the cluster/user it names. + contextIndex, err := findKubeConfigEntry(config.Contexts, config.CurrentContext, "context", + func(c kubeConfigContext) string { return c.Name }) + if err != nil { + return nil, err + } + + clusterName, userName := "", "" + if contextIndex >= 0 { + clusterName = config.Contexts[contextIndex].Context.Cluster + userName = config.Contexts[contextIndex].Context.User + } + + credentials := &kubernetesClusterCredentials{} + + cluster, err := findKubeConfigEntry(config.Clusters, clusterName, "cluster", + func(c kubeConfigCluster) string { return c.Name }) + if err != nil { + return nil, err + } + if cluster >= 0 { + caCertificate, err := decodeKubernetesClusterConfigValue( + config.Clusters[cluster].Cluster.CertificateAuthorityData, "certificate-authority-data") + if err != nil { + return nil, err + } + + credentials.Endpoint = config.Clusters[cluster].Cluster.Server + credentials.ClusterCACertificate = caCertificate + } + + user, err := findKubeConfigEntry(config.Users, userName, "user", + func(u kubeConfigUser) string { return u.Name }) + if err != nil { + return nil, err + } + if user >= 0 { + clientCertificate, err := decodeKubernetesClusterConfigValue( + config.Users[user].User.ClientCertificateData, "client-certificate-data") + if err != nil { + return nil, err + } + + clientKey, err := decodeKubernetesClusterConfigValue( + config.Users[user].User.ClientKeyData, "client-key-data") + if err != nil { + return nil, err + } + + credentials.ClientCertificate = clientCertificate + credentials.ClientKey = clientKey + } + + return credentials, nil +} + +// findKubeConfigEntry finds an entry by name, falling back to the sole entry +// when there's only one. With more than one and no match, it errors instead +// of guessing, since a wrong guess could pair the wrong cluster and user. +func findKubeConfigEntry[T any](entries []T, name string, kind string, nameOf func(T) string) (int, error) { + if len(entries) == 0 { + log.Printf("[WARN] Kubeconfig does not contain any %s", kind) + return -1, nil + } + + if name != "" { + if i := slices.IndexFunc(entries, func(e T) bool { return nameOf(e) == name }); i >= 0 { + return i, nil + } + + if len(entries) > 1 { + return -1, fmt.Errorf("kubeconfig does not define %s %q among its %d %s entries", + kind, name, len(entries), kind) + } + + log.Printf("[WARN] Kubeconfig does not define the %s %q, using the only %s available instead", kind, name, kind) + return 0, nil + } + + if len(entries) > 1 { + log.Printf("[WARN] Kubeconfig has %d %s entries and no current context to select one, using the first %s", len(entries), kind, kind) + } + + return 0, nil +} + +// decodeKubernetesClusterConfigValue decodes a base64 encoded kubeconfig field, +// passing an absent field through as an empty string. +func decodeKubernetesClusterConfigValue(value string, field string) (string, error) { + if value == "" { + log.Printf("[WARN] Kubeconfig does not contain %s", field) + return "", nil + } + + decoded, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return "", fmt.Errorf("Failed to decode %s: %s", field, err) + } + + return string(decoded), nil +} diff --git a/cloudstack/data_source_cloudstack_kubernetes_cluster_config_test.go b/cloudstack/data_source_cloudstack_kubernetes_cluster_config_test.go new file mode 100644 index 00000000..6cfe0ebd --- /dev/null +++ b/cloudstack/data_source_cloudstack_kubernetes_cluster_config_test.go @@ -0,0 +1,315 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "encoding/base64" + "fmt" + "strings" + "testing" +) + +func TestParseKubernetesClusterConfig(t *testing.T) { + tests := []struct { + name string + configData string + expected kubernetesClusterCredentials + expectErr string + }{ + { + name: "kubeadm admin.conf as returned by CloudStack", + configData: fmt.Sprintf(`apiVersion: v1 +kind: Config +preferences: {} +clusters: +- cluster: + certificate-authority-data: %s + server: https://10.1.1.1:6443 + name: kubernetes +contexts: +- context: + cluster: kubernetes + user: kubernetes-admin + name: kubernetes-admin@kubernetes +current-context: kubernetes-admin@kubernetes +users: +- name: kubernetes-admin + user: + client-certificate-data: %s + client-key-data: %s +`, testBase64("ca-certificate"), testBase64("client-certificate"), testBase64("client-key")), + expected: kubernetesClusterCredentials{ + Endpoint: "https://10.1.1.1:6443", + ClusterCACertificate: "ca-certificate", + ClientCertificate: "client-certificate", + ClientKey: "client-key", + }, + }, + { + // Guards against blindly taking clusters[0] / users[0]. + name: "current context selects the second cluster and user", + configData: testKubeConfigTwoClusters("second@second"), + expected: kubernetesClusterCredentials{ + Endpoint: "https://10.2.2.2:6443", + ClusterCACertificate: "second-ca-certificate", + ClientCertificate: "second-client-certificate", + ClientKey: "second-client-key", + }, + }, + { + name: "absent current context falls back to the first entry", + configData: testKubeConfigTwoClusters(""), + expected: kubernetesClusterCredentials{ + Endpoint: "https://10.1.1.1:6443", + ClusterCACertificate: "first-ca-certificate", + ClientCertificate: "first-client-certificate", + ClientKey: "first-client-key", + }, + }, + { + // Ambiguous among several contexts: error rather than guess. + name: "unknown current context among several contexts is an error", + configData: testKubeConfigTwoClusters("missing@missing"), + expectErr: "does not define", + }, + { + // Only one candidate exists, so this falls back instead of erroring. + name: "current context naming a missing cluster falls back to the only entry", + configData: fmt.Sprintf(`apiVersion: v1 +kind: Config +current-context: third@third +clusters: +- cluster: + certificate-authority-data: %s + server: https://10.1.1.1:6443 + name: first +contexts: +- context: + cluster: third + user: third + name: third@third +users: +- name: first + user: + client-certificate-data: %s + client-key-data: %s +`, testBase64("first-ca-certificate"), + testBase64("first-client-certificate"), testBase64("first-client-key")), + expected: kubernetesClusterCredentials{ + Endpoint: "https://10.1.1.1:6443", + ClusterCACertificate: "first-ca-certificate", + ClientCertificate: "first-client-certificate", + ClientKey: "first-client-key", + }, + }, + { + // Ambiguous among several clusters: error rather than guess. + // No users section needed; the cluster error returns first. + name: "current context naming an undefined cluster among several is an error", + configData: fmt.Sprintf(`apiVersion: v1 +kind: Config +current-context: third@third +clusters: +- cluster: + certificate-authority-data: %s + server: https://10.1.1.1:6443 + name: first +- cluster: + certificate-authority-data: %s + server: https://10.2.2.2:6443 + name: second +contexts: +- context: + cluster: third + user: third + name: third@third +`, testBase64("first-ca-certificate"), testBase64("second-ca-certificate")), + expectErr: "does not define", + }, + { + // Same error, but via the user branch instead of the cluster branch. + name: "current context naming an undefined user among several is an error", + configData: fmt.Sprintf(`apiVersion: v1 +kind: Config +current-context: kubernetes@kubernetes +clusters: +- cluster: + certificate-authority-data: %s + server: https://10.1.1.1:6443 + name: kubernetes +contexts: +- context: + cluster: kubernetes + user: third + name: kubernetes@kubernetes +users: +- name: first + user: + client-certificate-data: %s + client-key-data: %s +- name: second + user: + client-certificate-data: %s + client-key-data: %s +`, testBase64("ca-certificate"), + testBase64("first-client-certificate"), testBase64("first-client-key"), + testBase64("second-client-certificate"), testBase64("second-client-key")), + expectErr: "does not define", + }, + { + // A kubeconfig without client certificates must not fail the data + // source, so that config_data stays usable. + name: "no users leaves the client credentials empty", + configData: fmt.Sprintf(`apiVersion: v1 +kind: Config +clusters: +- cluster: + certificate-authority-data: %s + server: https://10.1.1.1:6443 + name: kubernetes +`, testBase64("ca-certificate")), + expected: kubernetesClusterCredentials{ + Endpoint: "https://10.1.1.1:6443", + ClusterCACertificate: "ca-certificate", + }, + }, + { + name: "no clusters leaves the endpoint empty", + configData: fmt.Sprintf(`apiVersion: v1 +kind: Config +users: +- name: kubernetes-admin + user: + client-certificate-data: %s + client-key-data: %s +`, testBase64("client-certificate"), testBase64("client-key")), + expected: kubernetesClusterCredentials{ + ClientCertificate: "client-certificate", + ClientKey: "client-key", + }, + }, + { + // The empty-config guard lives in the read function, not here. + name: "empty document yields no credentials and no error", + configData: "", + expected: kubernetesClusterCredentials{}, + }, + { + name: "invalid yaml", + configData: "\tthis is not a kubeconfig", + expectErr: "Invalid kubeconfig", + }, + { + name: "certificate authority data is not base64", + configData: `apiVersion: v1 +kind: Config +clusters: +- cluster: + certificate-authority-data: "@@@ not base64 @@@" + server: https://10.1.1.1:6443 + name: kubernetes +`, + expectErr: "Failed to decode certificate-authority-data", + }, + { + name: "client key data is not base64", + configData: fmt.Sprintf(`apiVersion: v1 +kind: Config +users: +- name: kubernetes-admin + user: + client-certificate-data: %s + client-key-data: "@@@ not base64 @@@" +`, testBase64("client-certificate")), + expectErr: "Failed to decode client-key-data", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + credentials, err := parseKubernetesClusterConfig(tt.configData) + + if tt.expectErr != "" { + if err == nil { + t.Fatalf("Expected an error containing %q, got none", tt.expectErr) + } + if !strings.Contains(err.Error(), tt.expectErr) { + t.Fatalf("Expected an error containing %q, got: %s", tt.expectErr, err) + } + return + } + + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + if *credentials != tt.expected { + t.Errorf("Expected %+v, got %+v", tt.expected, *credentials) + } + }) + } +} + +func testBase64(value string) string { + return base64.StdEncoding.EncodeToString([]byte(value)) +} + +// testKubeConfigTwoClusters renders a two-cluster, two-user kubeconfig; +// currentContext picks the context ("" omits current-context). Args are +// listed in template order so the substitution can be eyeballed. +func testKubeConfigTwoClusters(currentContext string) string { + return fmt.Sprintf(`apiVersion: v1 +kind: Config +current-context: %s +clusters: +- cluster: + certificate-authority-data: %s + server: https://10.1.1.1:6443 + name: first +- cluster: + certificate-authority-data: %s + server: https://10.2.2.2:6443 + name: second +contexts: +- context: + cluster: first + user: first + name: first@first +- context: + cluster: second + user: second + name: second@second +users: +- name: first + user: + client-certificate-data: %s + client-key-data: %s +- name: second + user: + client-certificate-data: %s + client-key-data: %s +`, + currentContext, + testBase64("first-ca-certificate"), + testBase64("second-ca-certificate"), + testBase64("first-client-certificate"), + testBase64("first-client-key"), + testBase64("second-client-certificate"), + testBase64("second-client-key")) +} diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 72090147..5e4b8515 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -78,33 +78,34 @@ func Provider() *schema.Provider { }, DataSourcesMap: map[string]*schema.Resource{ - "cloudstack_autoscale_policy": dataSourceCloudstackAutoscalePolicy(), - "cloudstack_autoscale_vm_group": dataSourceCloudstackAutoscaleVMGroup(), - "cloudstack_autoscale_vm_profile": dataSourceCloudstackAutoscaleVMProfile(), - "cloudstack_condition": dataSourceCloudstackCondition(), - "cloudstack_counter": dataSourceCloudstackCounter(), - "cloudstack_template": dataSourceCloudstackTemplate(), - "cloudstack_ssh_keypair": dataSourceCloudstackSSHKeyPair(), - "cloudstack_instance": dataSourceCloudstackInstance(), - "cloudstack_network_offering": dataSourceCloudstackNetworkOffering(), - "cloudstack_zone": dataSourceCloudStackZone(), - "cloudstack_service_offering": dataSourceCloudstackServiceOffering(), - "cloudstack_volume": dataSourceCloudstackVolume(), - "cloudstack_vpc": dataSourceCloudstackVPC(), - "cloudstack_ipaddress": dataSourceCloudstackIPAddress(), - "cloudstack_user": dataSourceCloudstackUser(), - "cloudstack_vpn_connection": dataSourceCloudstackVPNConnection(), - "cloudstack_pod": dataSourceCloudstackPod(), - "cloudstack_domain": dataSourceCloudstackDomain(), - "cloudstack_project": dataSourceCloudstackProject(), - "cloudstack_physical_network": dataSourceCloudStackPhysicalNetwork(), - "cloudstack_role": dataSourceCloudstackRole(), - "cloudstack_cluster": dataSourceCloudstackCluster(), - "cloudstack_limits": dataSourceCloudStackLimits(), - "cloudstack_quota": dataSourceCloudStackQuota(), - "cloudstack_quota_enabled": dataSourceCloudStackQuotaEnabled(), - "cloudstack_quota_tariff": dataSourceCloudStackQuotaTariff(), - "cloudstack_user_data": dataSourceCloudstackUserData(), + "cloudstack_autoscale_policy": dataSourceCloudstackAutoscalePolicy(), + "cloudstack_autoscale_vm_group": dataSourceCloudstackAutoscaleVMGroup(), + "cloudstack_autoscale_vm_profile": dataSourceCloudstackAutoscaleVMProfile(), + "cloudstack_condition": dataSourceCloudstackCondition(), + "cloudstack_counter": dataSourceCloudstackCounter(), + "cloudstack_template": dataSourceCloudstackTemplate(), + "cloudstack_ssh_keypair": dataSourceCloudstackSSHKeyPair(), + "cloudstack_instance": dataSourceCloudstackInstance(), + "cloudstack_network_offering": dataSourceCloudstackNetworkOffering(), + "cloudstack_zone": dataSourceCloudStackZone(), + "cloudstack_service_offering": dataSourceCloudstackServiceOffering(), + "cloudstack_volume": dataSourceCloudstackVolume(), + "cloudstack_vpc": dataSourceCloudstackVPC(), + "cloudstack_ipaddress": dataSourceCloudstackIPAddress(), + "cloudstack_kubernetes_cluster_config": dataSourceCloudstackKubernetesClusterConfig(), + "cloudstack_user": dataSourceCloudstackUser(), + "cloudstack_vpn_connection": dataSourceCloudstackVPNConnection(), + "cloudstack_pod": dataSourceCloudstackPod(), + "cloudstack_domain": dataSourceCloudstackDomain(), + "cloudstack_project": dataSourceCloudstackProject(), + "cloudstack_physical_network": dataSourceCloudStackPhysicalNetwork(), + "cloudstack_role": dataSourceCloudstackRole(), + "cloudstack_cluster": dataSourceCloudstackCluster(), + "cloudstack_limits": dataSourceCloudStackLimits(), + "cloudstack_quota": dataSourceCloudStackQuota(), + "cloudstack_quota_enabled": dataSourceCloudStackQuotaEnabled(), + "cloudstack_quota_tariff": dataSourceCloudStackQuotaTariff(), + "cloudstack_user_data": dataSourceCloudstackUserData(), }, ResourcesMap: map[string]*schema.Resource{ diff --git a/go.mod b/go.mod index 6e69966d..af62d755 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/hashicorp/terraform-plugin-mux v0.16.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 github.com/hashicorp/terraform-plugin-testing v1.7.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/website/docs/d/kubernetes_cluster_config.html.markdown b/website/docs/d/kubernetes_cluster_config.html.markdown new file mode 100644 index 00000000..5202d3b6 --- /dev/null +++ b/website/docs/d/kubernetes_cluster_config.html.markdown @@ -0,0 +1,96 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_kubernetes_cluster_config" +sidebar_current: "docs-cloudstack-datasource-kubernetes-cluster-config" +description: |- + Get the kubeconfig of a CloudStack Kubernetes cluster. +--- + +# cloudstack_kubernetes_cluster_config + +Use this data source to retrieve the kubeconfig of a CloudStack Kubernetes (CKS) cluster, so that +the `kubernetes` and `helm` providers can be pointed at a cluster managed by Terraform. + +The cluster must be running for its config to be available. CloudStack does not return a config +while a cluster is still being set up, or when the Kubernetes service plugin is disabled +(`cloud.kubernetes.service.enabled`). + +~> **Note:** The kubeconfig contains the cluster administrator credentials, and every attribute of +this data source is stored in plain text in your Terraform state. Protect the state accordingly, +for example by using a remote backend with encryption at rest. + +## Example Usage + +```hcl +resource "cloudstack_kubernetes_cluster" "basic" { + name = "basic-cluster" + zone = "zone1" + kubernetes_version = "1.25.0" + service_offering = "Medium Instance" + size = 3 + description = "Basic Kubernetes cluster" +} + +data "cloudstack_kubernetes_cluster_config" "basic" { + cluster_id = cloudstack_kubernetes_cluster.basic.id +} +``` + +### Configuring the Kubernetes and Helm providers + +```hcl +provider "kubernetes" { + host = data.cloudstack_kubernetes_cluster_config.basic.endpoint + cluster_ca_certificate = data.cloudstack_kubernetes_cluster_config.basic.cluster_ca_certificate + client_certificate = data.cloudstack_kubernetes_cluster_config.basic.client_certificate + client_key = data.cloudstack_kubernetes_cluster_config.basic.client_key +} + +provider "helm" { + kubernetes { + host = data.cloudstack_kubernetes_cluster_config.basic.endpoint + cluster_ca_certificate = data.cloudstack_kubernetes_cluster_config.basic.cluster_ca_certificate + client_certificate = data.cloudstack_kubernetes_cluster_config.basic.client_certificate + client_key = data.cloudstack_kubernetes_cluster_config.basic.client_key + } +} +``` + +### Writing the kubeconfig to a file + +```hcl +resource "local_sensitive_file" "kubeconfig" { + filename = "${path.module}/kubeconfig" + content = data.cloudstack_kubernetes_cluster_config.basic.config_data +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cluster_id` - (Required) The ID of the Kubernetes cluster to retrieve the config for. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the Kubernetes cluster. +* `name` - The name of the Kubernetes cluster. +* `config_data` - The raw kubeconfig of the Kubernetes cluster. +* `endpoint` - The URL of the Kubernetes API server. +* `cluster_ca_certificate` - The PEM encoded certificate authority of the Kubernetes API server. +* `client_certificate` - The PEM encoded client certificate used to authenticate against the + Kubernetes API server. +* `client_key` - The PEM encoded client key used to authenticate against the Kubernetes API server. + +The `endpoint`, `cluster_ca_certificate`, `client_certificate` and `client_key` attributes are +extracted from `config_data`, resolved through the kubeconfig's current context, and base64 decoded +where applicable. If a kubeconfig does not carry one of them, the corresponding attribute is empty +and `config_data` can be used directly instead: + +```hcl +locals { + kubeconfig = yamldecode(data.cloudstack_kubernetes_cluster_config.basic.config_data) +} +```