Skip to content

Commit

Permalink
Merge pull request #12372 from hashicorp/f-kubernetes
Browse files Browse the repository at this point in the history
kubernetes: Add provider + namespace resource
  • Loading branch information
radeksimko authored Mar 16, 2017
2 parents d1eab51 + eb08f40 commit 4448e45
Show file tree
Hide file tree
Showing 14 changed files with 1,055 additions and 0 deletions.
171 changes: 171 additions & 0 deletions builtin/providers/kubernetes/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package kubernetes

import (
"bytes"
"fmt"
"log"
"os"

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-homedir"
kubernetes "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)

func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"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.",
},
"config_path": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG", "~/.kube/config"),
Description: "Path to the kube config file, defaults to ~/.kube/config",
},
"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: "",
},
},

ResourcesMap: map[string]*schema.Resource{
"kubernetes_namespace": resourceKubernetesNamespace(),
},
ConfigureFunc: providerConfigure,
}
}

func providerConfigure(d *schema.ResourceData) (interface{}, error) {
// Config file loading
cfg, err := tryLoadingConfigFile(d)
if err != nil {
return nil, err
}
if cfg == nil {
cfg = &restclient.Config{}
}

// Overriding with static configuration
cfg.UserAgent = fmt.Sprintf("HashiCorp/1.0 Terraform/%s", terraform.VersionString())

if v, ok := d.GetOk("host"); ok {
cfg.Host = v.(string)
}
if v, ok := d.GetOk("username"); ok {
cfg.Username = v.(string)
}
if v, ok := d.GetOk("password"); ok {
cfg.Password = v.(string)
}
if v, ok := d.GetOk("insecure"); ok {
cfg.Insecure = v.(bool)
}
if v, ok := d.GetOk("cluster_ca_certificate"); ok {
cfg.CAData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := d.GetOk("client_certificate"); ok {
cfg.CertData = bytes.NewBufferString(v.(string)).Bytes()
}
if v, ok := d.GetOk("client_key"); ok {
cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes()
}

k, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, fmt.Errorf("Failed to configure: %s", err)
}

return k, nil
}

func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) {
path, err := homedir.Expand(d.Get("config_path").(string))
if err != nil {
return nil, err
}

loader := &clientcmd.ClientConfigLoadingRules{
ExplicitPath: path,
}
overrides := &clientcmd.ConfigOverrides{}
ctxSuffix := "; no context"
authInfo, authInfoOk := d.GetOk("config_context_auth_info")
cluster, clusterOk := d.GetOk("config_context_cluster")
if authInfoOk || clusterOk {
overrides.Context = clientcmdapi.Context{}
if authInfoOk {
overrides.Context.AuthInfo = authInfo.(string)
}
if clusterOk {
overrides.Context.Cluster = cluster.(string)
}
ctxSuffix = fmt.Sprintf("; auth_info: %s, cluster: %s",
overrides.Context.AuthInfo, overrides.Context.Cluster)
}
log.Printf("[DEBUG] Using override context: %#v", *overrides)

cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides)
cfg, err := cc.ClientConfig()
if err != nil {
if pathErr, ok := err.(*os.PathError); ok && os.IsNotExist(pathErr.Err) {
log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", path)
return nil, nil
}
return nil, fmt.Errorf("Failed to load config (%s%s): %s", path, ctxSuffix, err)
}

log.Printf("[INFO] Successfully loaded config file (%s%s)", path, ctxSuffix)
return cfg, nil
}
53 changes: 53 additions & 0 deletions builtin/providers/kubernetes/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package kubernetes

import (
"os"
"strings"
"testing"

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)

var testAccProviders map[string]terraform.ResourceProvider
var testAccProvider *schema.Provider

func init() {
testAccProvider = Provider().(*schema.Provider)
testAccProviders = map[string]terraform.ResourceProvider{
"kubernetes": testAccProvider,
}
}

func TestProvider(t *testing.T) {
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}

func TestProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = Provider()
}

func testAccPreCheck(t *testing.T) {
hasFileCfg := (os.Getenv("KUBE_CTX_AUTH_INFO") != "" && os.Getenv("KUBE_CTX_CLUSTER") != "")
hasStaticCfg := (os.Getenv("KUBE_HOST") != "" &&
os.Getenv("KUBE_USER") != "" &&
os.Getenv("KUBE_PASSWORD") != "" &&
os.Getenv("KUBE_CLIENT_CERT_DATA") != "" &&
os.Getenv("KUBE_CLIENT_KEY_DATA") != "" &&
os.Getenv("KUBE_CLUSTER_CA_CERT_DATA") != "")

if !hasFileCfg && !hasStaticCfg {
t.Fatalf("File config (KUBE_CTX_AUTH_INFO and KUBE_CTX_CLUSTER) or static configuration"+
" (%s) must be set for acceptance tests",
strings.Join([]string{
"KUBE_HOST",
"KUBE_USER",
"KUBE_PASSWORD",
"KUBE_CLIENT_CERT_DATA",
"KUBE_CLIENT_KEY_DATA",
"KUBE_CLUSTER_CA_CERT_DATA",
}, ", "))
}
}
143 changes: 143 additions & 0 deletions builtin/providers/kubernetes/resource_kubernetes_namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package kubernetes

import (
"fmt"
"log"
"time"

"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"k8s.io/kubernetes/pkg/api/errors"
api "k8s.io/kubernetes/pkg/api/v1"
kubernetes "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
)

func resourceKubernetesNamespace() *schema.Resource {
return &schema.Resource{
Create: resourceKubernetesNamespaceCreate,
Read: resourceKubernetesNamespaceRead,
Exists: resourceKubernetesNamespaceExists,
Update: resourceKubernetesNamespaceUpdate,
Delete: resourceKubernetesNamespaceDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"metadata": metadataSchema("namespace"),
},
}
}

func resourceKubernetesNamespaceCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)

metadata := expandMetadata(d.Get("metadata").([]interface{}))
namespace := api.Namespace{
ObjectMeta: metadata,
}
log.Printf("[INFO] Creating new namespace: %#v", namespace)
out, err := conn.CoreV1().Namespaces().Create(&namespace)
if err != nil {
return err
}
log.Printf("[INFO] Submitted new namespace: %#v", out)
d.SetId(out.Name)

return resourceKubernetesNamespaceRead(d, meta)
}

func resourceKubernetesNamespaceRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)

name := d.Id()
log.Printf("[INFO] Reading namespace %s", name)
namespace, err := conn.CoreV1().Namespaces().Get(name)
if err != nil {
log.Printf("[DEBUG] Received error: %#v", err)
return err
}
log.Printf("[INFO] Received namespace: %#v", namespace)
err = d.Set("metadata", flattenMetadata(namespace.ObjectMeta))
if err != nil {
return err
}

return nil
}

func resourceKubernetesNamespaceUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)

metadata := expandMetadata(d.Get("metadata").([]interface{}))
// This is necessary in case the name is generated
metadata.Name = d.Id()

namespace := api.Namespace{
ObjectMeta: metadata,
}
log.Printf("[INFO] Updating namespace: %#v", namespace)
out, err := conn.CoreV1().Namespaces().Update(&namespace)
if err != nil {
return err
}
log.Printf("[INFO] Submitted updated namespace: %#v", out)
d.SetId(out.Name)

return resourceKubernetesNamespaceRead(d, meta)
}

func resourceKubernetesNamespaceDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*kubernetes.Clientset)

name := d.Id()
log.Printf("[INFO] Deleting namespace: %#v", name)
err := conn.CoreV1().Namespaces().Delete(name, &api.DeleteOptions{})
if err != nil {
return err
}

stateConf := &resource.StateChangeConf{
Target: []string{},
Pending: []string{"Terminating"},
Timeout: 5 * time.Minute,
Refresh: func() (interface{}, string, error) {
out, err := conn.CoreV1().Namespaces().Get(name)
if err != nil {
if statusErr, ok := err.(*errors.StatusError); ok && statusErr.ErrStatus.Code == 404 {
return nil, "", nil
}
log.Printf("[ERROR] Received error: %#v", err)
return out, "Error", err
}

statusPhase := fmt.Sprintf("%v", out.Status.Phase)
log.Printf("[DEBUG] Namespace %s status received: %#v", out.Name, statusPhase)
return out, statusPhase, nil
},
}
_, err = stateConf.WaitForState()
if err != nil {
return err
}
log.Printf("[INFO] Namespace %s deleted", name)

d.SetId("")
return nil
}

func resourceKubernetesNamespaceExists(d *schema.ResourceData, meta interface{}) (bool, error) {
conn := meta.(*kubernetes.Clientset)

name := d.Id()
log.Printf("[INFO] Checking namespace %s", name)
_, err := conn.CoreV1().Namespaces().Get(name)
if err != nil {
if statusErr, ok := err.(*errors.StatusError); ok && statusErr.ErrStatus.Code == 404 {
return false, nil
}
log.Printf("[DEBUG] Received error: %#v", err)
}
log.Printf("[INFO] Namespace %s exists", name)
return true, err
}
Loading

0 comments on commit 4448e45

Please sign in to comment.