Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 283 additions & 0 deletions cloudstack/data_source_cloudstack_kubernetes_cluster_config.go
Original file line number Diff line number Diff line change
@@ -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)
}
Comment on lines +149 to +162

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
}
Comment thread
sudo87 marked this conversation as resolved.

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
}
Loading
Loading