Skip to content

Commit

Permalink
add default interface
Browse files Browse the repository at this point in the history
  • Loading branch information
floreks committed Nov 16, 2023
1 parent dee791c commit fb6dba8
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 0 deletions.
34 changes: 34 additions & 0 deletions internal/defaults/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package defaults

import (
"context"
"fmt"
"os"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func Env[T any, R any](envVar string, defaultValue T) R {
return envDefaultValue{
envVar: envVar,
defaultValue: defaultValue,
}
}

type envDefaultValue struct {
envVar string
defaultValue string
}

func (d envDefaultValue) Description(_ context.Context) string {
return fmt.Sprintf("If value is not configured, defaults to a string representation of the provided env variable")
}

func (d envDefaultValue) MarkdownDescription(_ context.Context) string {
return fmt.Sprintf("If value is not configured, defaults to a string representation of the provided env variable")
}

func (d envDefaultValue) DefaultString(_ context.Context, req defaults.StringRequest, resp *defaults.StringResponse) {
resp.PlanValue = types.StringValue(os.Getenv(d.envVar))
}
25 changes: 25 additions & 0 deletions internal/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,28 @@ type Cluster struct {
Protect types.Bool `tfsdk:"protect"`
Tags types.Map `tfsdk:"tags"`
}

type Kubeconfig struct {
Host types.String `tfsdk:"host"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
Inscure types.String `tfsdk:"insecure"`
TlsServerName types.String `tfsdk:"tls_server_name"`
ClientCertificate types.String `tfsdk:"client_certificate"`
ClientKey types.String `tfsdk:"client_key"`
ClusterCACertificate types.String `tfsdk:"cluster_ca_certificate"`
ConfigPath types.String `tfsdk:"config_path"`
ConfigContext types.String `tfsdk:"config_context"`
ConfigContextAuthInfo types.String `tfsdk:"config_context_auth_info"`
ConfigContextCluster types.String `tfsdk:"config_context_cluster"`
Token types.String `tfsdk:"token"`
ProxyURL types.String `tfsdk:"proxy_url"`
Exec types.Object `tfsdk:"exec"`
}

type KubeconfigExec struct {
Command types.String `tfsdk:"command"`
Args types.String `tfsdk:"args"`
Env types.String `tfsdk:"env"`
APIVersion types.String `tfsdk:"api_version"`
}
187 changes: 187 additions & 0 deletions internal/resource/cluster_kubeconfig.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
package resource

import (
"bytes"
"fmt"
"os"
"sync"

"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mitchellh/go-homedir"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/clientcmd"

"terraform-provider-plural/internal/model"
)

func kubeconfigAttribute() schema.SingleNestedAttribute {
Expand All @@ -13,10 +26,12 @@ func kubeconfigAttribute() schema.SingleNestedAttribute {
Attributes: map[string]schema.Attribute{
"host": schema.StringAttribute{
Optional: true,
//Default: defaults.EnvString("PLURAL_KUBE_HOST", ""),
MarkdownDescription: "The address of the Kubernetes clusters. Can be sourced from `PLURAL_KUBE_HOST`.",
},
"username": schema.StringAttribute{
Optional: true,
//Default: defaults.EnvString("PLURAL_KUBE_HOST", ""),
MarkdownDescription: "The username for basic authentication to the Kubernetes cluster. Can be sourced from `PLURAL_KUBE_USER`.",
},
"password": schema.StringAttribute{
Expand Down Expand Up @@ -101,3 +116,175 @@ func kubeconfigAttribute() schema.SingleNestedAttribute {
},
}
}

// KubeConfig is a RESTClientGetter interface implementation
type KubeConfig struct {
ClientConfig clientcmd.ClientConfig

Burst int

sync.Mutex
}

// ToRESTConfig implemented interface method
func (k *KubeConfig) ToRESTConfig() (*rest.Config, error) {
config, err := k.ToRawKubeConfigLoader().ClientConfig()
return config, err
}

// ToDiscoveryClient implemented interface method
func (k *KubeConfig) ToDiscoveryClient() (discovery.DiscoveryInterface, error) {
config, err := k.ToRESTConfig()
if err != nil {
return nil, err
}

// The more groups you have, the more discovery requests you need to make.
// given 25 groups (our groups + a few custom resources) with one-ish version each, discovery needs to make 50 requests
// double it just so we don't end up here again for a while. This config is only used for discovery.
config.Burst = k.Burst

return discovery.NewDiscoveryClientForConfigOrDie(config), nil
}

// ToRESTMapper implemented interface method
func (k *KubeConfig) ToRESTMapper() (meta.RESTMapper, error) {
discoveryClient, err := k.ToDiscoveryClient()
if err != nil {
return nil, err
}

mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)

Check failure on line 157 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Unit tests

cannot use discoveryClient (variable of type discovery.DiscoveryInterface) as discovery.CachedDiscoveryInterface value in argument to restmapper.NewDeferredDiscoveryRESTMapper: discovery.DiscoveryInterface does not implement discovery.CachedDiscoveryInterface (missing method Fresh)

Check failure on line 157 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Build

cannot use discoveryClient (variable of type discovery.DiscoveryInterface) as discovery.CachedDiscoveryInterface value in argument to restmapper.NewDeferredDiscoveryRESTMapper: discovery.DiscoveryInterface does not implement discovery.CachedDiscoveryInterface (missing method Fresh)
expander := restmapper.NewShortcutExpander(mapper, discoveryClient)
return expander, nil
}

// ToRawKubeConfigLoader implemented interface method
func (k *KubeConfig) ToRawKubeConfigLoader() clientcmd.ClientConfig {
return k.ClientConfig
}

func newKubeConfig(configData *model.Kubeconfig, namespace *string) (*KubeConfig, error) {
overrides := &clientcmd.ConfigOverrides{}
loader := &clientcmd.ClientConfigLoadingRules{}

if v := os.Getenv("PLURAL_KUBE_CONFIG_PATH"); v != "" {
configData.ConfigPath = types.StringValue(v)
}

if len(configPaths) > 0 {

Check failure on line 175 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Unit tests

undefined: configPaths

Check failure on line 175 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Build

undefined: configPaths
expandedPaths := []string{}
for _, p := range configPaths {

Check failure on line 177 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Unit tests

undefined: configPaths

Check failure on line 177 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Build

undefined: configPaths
path, err := homedir.Expand(p)
if err != nil {
return nil, err
}

log.Printf("[DEBUG] Using kubeconfig: %s", path)

Check failure on line 183 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Unit tests

undefined: log

Check failure on line 183 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Build

undefined: log
expandedPaths = append(expandedPaths, path)
}

if len(expandedPaths) == 1 {
loader.ExplicitPath = expandedPaths[0]
} else {
loader.Precedence = expandedPaths
}

ctx, ctxOk := k8sGetOk(configData, "config_context")

Check failure on line 193 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Unit tests

undefined: k8sGetOk

Check failure on line 193 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Build

undefined: k8sGetOk
authInfo, authInfoOk := k8sGetOk(configData, "config_context_auth_info")

Check failure on line 194 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Unit tests

undefined: k8sGetOk

Check failure on line 194 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Build

undefined: k8sGetOk
cluster, clusterOk := k8sGetOk(configData, "config_context_cluster")

Check failure on line 195 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Unit tests

undefined: k8sGetOk

Check failure on line 195 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Build

undefined: k8sGetOk
if ctxOk || authInfoOk || clusterOk {
if ctxOk {
overrides.CurrentContext = ctx.(string)
log.Printf("[DEBUG] Using custom current context: %q", overrides.CurrentContext)

Check failure on line 199 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Unit tests

undefined: log

Check failure on line 199 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Build

undefined: log
}

overrides.Context = clientcmdapi.Context{}

Check failure on line 202 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Unit tests

undefined: clientcmdapi

Check failure on line 202 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Build

undefined: clientcmdapi
if authInfoOk {
overrides.Context.AuthInfo = authInfo.(string)
}
if clusterOk {
overrides.Context.Cluster = cluster.(string)
}
log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context)

Check failure on line 209 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Unit tests

undefined: log

Check failure on line 209 in internal/resource/cluster_kubeconfig.go

View workflow job for this annotation

GitHub Actions / Build

undefined: log
}
}

// Overriding with static configuration
if v, ok := k8sGetOk(configData, "insecure"); ok {
overrides.ClusterInfo.InsecureSkipTLSVerify = v.(bool)
}
if v, ok := k8sGetOk(configData, "tls_server_name"); ok {
overrides.ClusterInfo.TLSServerName = v.(string)
}
if v, ok := k8sGetOk(configData, "cluster_ca_certificate"); ok {
overrides.ClusterInfo.CertificateAuthorityData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := k8sGetOk(configData, "client_certificate"); ok {
overrides.AuthInfo.ClientCertificateData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := k8sGetOk(configData, "host"); ok {
// Server has to be the complete address of the kubernetes cluster (scheme://hostname:port), not just the hostname,
// because `overrides` are processed too late to be taken into account by `defaultServerUrlFor()`.
// This basically replicates what defaultServerUrlFor() does with config but for overrides,
// see https://github.com/kubernetes/client-go/blob/v12.0.0/rest/url_utils.go#L85-L87
hasCA := len(overrides.ClusterInfo.CertificateAuthorityData) != 0
hasCert := len(overrides.AuthInfo.ClientCertificateData) != 0
defaultTLS := hasCA || hasCert || overrides.ClusterInfo.InsecureSkipTLSVerify
host, _, err := rest.DefaultServerURL(v.(string), "", apimachineryschema.GroupVersion{}, defaultTLS)
if err != nil {
return nil, err
}

overrides.ClusterInfo.Server = host.String()
}
if v, ok := k8sGetOk(configData, "username"); ok {
overrides.AuthInfo.Username = v.(string)
}
if v, ok := k8sGetOk(configData, "password"); ok {
overrides.AuthInfo.Password = v.(string)
}
if v, ok := k8sGetOk(configData, "client_key"); ok {
overrides.AuthInfo.ClientKeyData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := k8sGetOk(configData, "token"); ok {
overrides.AuthInfo.Token = v.(string)
}

if v, ok := k8sGetOk(configData, "proxy_url"); ok {
overrides.ClusterDefaults.ProxyURL = v.(string)
}

if v, ok := k8sGetOk(configData, "exec"); ok {
exec := &clientcmdapi.ExecConfig{}
if spec, ok := v.([]interface{})[0].(map[string]interface{}); ok {
exec.InteractiveMode = clientcmdapi.IfAvailableExecInteractiveMode
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 {
log.Printf("[ERROR] Failed to parse exec")
return nil, fmt.Errorf("failed to parse exec")
}
overrides.AuthInfo.Exec = exec
}

overrides.Context.Namespace = "default"

if namespace != nil {
overrides.Context.Namespace = *namespace
}
burstLimit := configData.Get("burst_limit").(int)

client := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides)
if client == nil {
log.Printf("[ERROR] Failed to initialize kubernetes config")
return nil, nil
}
log.Printf("[INFO] Successfully initialized kubernetes config")

return &KubeConfig{ClientConfig: client, Burst: burstLimit}, nil
}

0 comments on commit fb6dba8

Please sign in to comment.