This is an automated email from the ASF dual-hosted git repository.
sureshanaparti pushed a commit to branch main
in repository
https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git
The following commit(s) were added to refs/heads/main by this push:
new 9b70d14 Add cloudstack_kubernetes_cluster_config data source (#312)
9b70d14 is described below
commit 9b70d141dc555f6d8d34b6a17e22c1fa753df636
Author: Manoj Kumar <[email protected]>
AuthorDate: Mon Aug 17 14:45:05 2026 +0530
Add cloudstack_kubernetes_cluster_config data source (#312)
* Add cloudstack_kubernetes_cluster_config data source
CloudStack's CKS clusters expose their kubeconfig only through the
getKubernetesClusterConfig API, with no Terraform-native way to feed it
into the kubernetes/helm providers. This adds a data source that fetches
it and parses out endpoint, cluster_ca_certificate, client_certificate,
and client_key as separate attributes named to match those providers'
own arguments, alongside the raw config_data for anything else.
Context resolution deliberately refuses to guess: if a kubeconfig's
current-context names a cluster or user absent from a list of more than
one entry, parsing errors instead of silently falling back, since a
wrong guess there would silently serve one cluster's endpoint paired
with a different cluster's credentials.
Closes #276.
---
..._source_cloudstack_kubernetes_cluster_config.go | 283 ++++++++++++++++++
...ce_cloudstack_kubernetes_cluster_config_test.go | 315 +++++++++++++++++++++
cloudstack/provider.go | 55 ++--
go.mod | 1 +
.../docs/d/kubernetes_cluster_config.html.markdown | 96 +++++++
5 files changed, 723 insertions(+), 27 deletions(-)
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 0000000..3116aca
--- /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 0000000..6cfe0eb
--- /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 bad42dc..4eba442 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 1a2a7fc..cb0d9c0 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 0000000..5202d3b
--- /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)
+}
+```