diff --git a/builtin/providers/kubernetes/provider.go b/builtin/providers/kubernetes/provider.go new file mode 100644 index 000000000..4670024e3 --- /dev/null +++ b/builtin/providers/kubernetes/provider.go @@ -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 +} diff --git a/builtin/providers/kubernetes/provider_test.go b/builtin/providers/kubernetes/provider_test.go new file mode 100644 index 000000000..fbea586a9 --- /dev/null +++ b/builtin/providers/kubernetes/provider_test.go @@ -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", + }, ", ")) + } +} diff --git a/builtin/providers/kubernetes/resource_kubernetes_namespace.go b/builtin/providers/kubernetes/resource_kubernetes_namespace.go new file mode 100644 index 000000000..9e6160e51 --- /dev/null +++ b/builtin/providers/kubernetes/resource_kubernetes_namespace.go @@ -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 +} diff --git a/builtin/providers/kubernetes/resource_kubernetes_namespace_test.go b/builtin/providers/kubernetes/resource_kubernetes_namespace_test.go new file mode 100644 index 000000000..561f8a01f --- /dev/null +++ b/builtin/providers/kubernetes/resource_kubernetes_namespace_test.go @@ -0,0 +1,272 @@ +package kubernetes + +import ( + "fmt" + "reflect" + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + api "k8s.io/kubernetes/pkg/api/v1" + kubernetes "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5" +) + +func TestAccKubernetesNamespace_basic(t *testing.T) { + var conf api.Namespace + nsName := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "kubernetes_namespace.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesNamespaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesNamespaceConfig_basic(nsName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesNamespaceExists("kubernetes_namespace.test", &conf), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.annotations.%", "2"), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.annotations.TestAnnotationOne", "one"), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.annotations.TestAnnotationTwo", "two"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{"TestAnnotationOne": "one", "TestAnnotationTwo": "two"}), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.labels.%", "3"), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.labels.TestLabelOne", "one"), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.labels.TestLabelTwo", "two"), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.labels.TestLabelThree", "three"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{"TestLabelOne": "one", "TestLabelTwo": "two", "TestLabelThree": "three"}), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.name", nsName), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.uid"), + ), + }, + { + Config: testAccKubernetesNamespaceConfig_smallerLists(nsName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesNamespaceExists("kubernetes_namespace.test", &conf), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.annotations.%", "2"), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.annotations.TestAnnotationOne", "one"), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.annotations.Different", "1234"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{"TestAnnotationOne": "one", "Different": "1234"}), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.labels.%", "2"), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.labels.TestLabelOne", "one"), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.labels.TestLabelThree", "three"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{"TestLabelOne": "one", "TestLabelThree": "three"}), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.name", nsName), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.uid"), + ), + }, + { + Config: testAccKubernetesNamespaceConfig_noLists(nsName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesNamespaceExists("kubernetes_namespace.test", &conf), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.annotations.%", "0"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.labels.%", "0"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.name", nsName), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.uid"), + ), + }, + }, + }) +} + +func TestAccKubernetesNamespace_importBasic(t *testing.T) { + resourceName := "kubernetes_namespace.test" + nsName := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesNamespaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesNamespaceConfig_basic(nsName), + }, + + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccKubernetesNamespace_generatedName(t *testing.T) { + var conf api.Namespace + prefix := "tf-acc-test-gen-" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "kubernetes_namespace.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesNamespaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesNamespaceConfig_generatedName(prefix), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesNamespaceExists("kubernetes_namespace.test", &conf), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.annotations.%", "0"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.labels.%", "0"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_namespace.test", "metadata.0.generate_name", prefix), + resource.TestMatchResourceAttr("kubernetes_namespace.test", "metadata.0.name", regexp.MustCompile("^"+prefix)), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_namespace.test", "metadata.0.uid"), + ), + }, + }, + }) +} + +func TestAccKubernetesNamespace_importGeneratedName(t *testing.T) { + resourceName := "kubernetes_namespace.test" + prefix := "tf-acc-test-gen-import-" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesNamespaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesNamespaceConfig_generatedName(prefix), + }, + + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckMetaAnnotations(om *api.ObjectMeta, expected map[string]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if len(expected) == 0 && len(om.Annotations) == 0 { + return nil + } + if !reflect.DeepEqual(om.Annotations, expected) { + return fmt.Errorf("%s annotations don't match.\nExpected: %q\nGiven: %q", + om.Name, expected, om.Annotations) + } + return nil + } +} + +func testAccCheckMetaLabels(om *api.ObjectMeta, expected map[string]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if len(expected) == 0 && len(om.Labels) == 0 { + return nil + } + if !reflect.DeepEqual(om.Labels, expected) { + return fmt.Errorf("%s labels don't match.\nExpected: %q\nGiven: %q", + om.Name, expected, om.Labels) + } + return nil + } +} + +func testAccCheckKubernetesNamespaceDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*kubernetes.Clientset) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "kubernetes_namespace" { + continue + } + + resp, err := conn.CoreV1().Namespaces().Get(rs.Primary.ID) + if err == nil { + if resp.Name == rs.Primary.ID { + return fmt.Errorf("Namespace still exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +func testAccCheckKubernetesNamespaceExists(n string, obj *api.Namespace) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*kubernetes.Clientset) + out, err := conn.CoreV1().Namespaces().Get(rs.Primary.ID) + if err != nil { + return err + } + + *obj = *out + return nil + } +} + +func testAccKubernetesNamespaceConfig_basic(nsName string) string { + return fmt.Sprintf(` +resource "kubernetes_namespace" "test" { + metadata { + annotations { + TestAnnotationOne = "one" + TestAnnotationTwo = "two" + } + labels { + TestLabelOne = "one" + TestLabelTwo = "two" + TestLabelThree = "three" + } + name = "%s" + } +}`, nsName) +} + +func testAccKubernetesNamespaceConfig_smallerLists(nsName string) string { + return fmt.Sprintf(` +resource "kubernetes_namespace" "test" { + metadata { + annotations { + TestAnnotationOne = "one" + Different = "1234" + } + labels { + TestLabelOne = "one" + TestLabelThree = "three" + } + name = "%s" + } +}`, nsName) +} + +func testAccKubernetesNamespaceConfig_noLists(nsName string) string { + return fmt.Sprintf(` +resource "kubernetes_namespace" "test" { + metadata { + name = "%s" + } +}`, nsName) +} + +func testAccKubernetesNamespaceConfig_generatedName(prefix string) string { + return fmt.Sprintf(` +resource "kubernetes_namespace" "test" { + metadata { + generate_name = "%s" + } +}`, prefix) +} diff --git a/builtin/providers/kubernetes/schema_metadata.go b/builtin/providers/kubernetes/schema_metadata.go new file mode 100644 index 000000000..8a3b3efaf --- /dev/null +++ b/builtin/providers/kubernetes/schema_metadata.go @@ -0,0 +1,75 @@ +package kubernetes + +import ( + "fmt" + + "github.com/hashicorp/terraform/helper/schema" +) + +func metadataFields(objectName string) map[string]*schema.Schema { + return map[string]*schema.Schema{ + "annotations": { + Type: schema.TypeMap, + Description: fmt.Sprintf("An unstructured key value map stored with the %s that may be used to store arbitrary metadata. More info: http://kubernetes.io/docs/user-guide/annotations", objectName), + Optional: true, + ValidateFunc: validateAnnotations, + }, + "generation": { + Type: schema.TypeInt, + Description: "A sequence number representing a specific generation of the desired state.", + Computed: true, + }, + "labels": { + Type: schema.TypeMap, + Description: fmt.Sprintf("Map of string keys and values that can be used to organize and categorize (scope and select) the %s. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels", objectName), + Optional: true, + ValidateFunc: validateLabels, + }, + "name": { + Type: schema.TypeString, + Description: fmt.Sprintf("Name of the %s, must be unique. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", objectName), + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validateName, + ConflictsWith: []string{"metadata.generate_name"}, + }, + "resource_version": { + Type: schema.TypeString, + Description: fmt.Sprintf("An opaque value that represents the internal version of this %s that can be used by clients to determine when %s has changed. Read more: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency", objectName, objectName), + Computed: true, + }, + "self_link": { + Type: schema.TypeString, + Description: fmt.Sprintf("A URL representing this %s.", objectName), + Computed: true, + }, + "uid": { + Type: schema.TypeString, + Description: fmt.Sprintf("The unique in time and space value for this %s. More info: http://kubernetes.io/docs/user-guide/identifiers#uids", objectName), + Computed: true, + }, + } +} + +func metadataSchema(objectName string) *schema.Schema { + fields := metadataFields(objectName) + fields["generate_name"] = &schema.Schema{ + Type: schema.TypeString, + Description: "Prefix, used by the server, to generate a unique name ONLY IF the `name` field has not been provided. This value will also be combined with a unique suffix. Read more: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#idempotency", + Optional: true, + ForceNew: true, + ValidateFunc: validateGenerateName, + ConflictsWith: []string{"metadata.name"}, + } + + return &schema.Schema{ + Type: schema.TypeList, + Description: fmt.Sprintf("Standard %s's metadata. More info: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#metadata", objectName), + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: fields, + }, + } +} diff --git a/builtin/providers/kubernetes/structures.go b/builtin/providers/kubernetes/structures.go new file mode 100644 index 000000000..5cf894376 --- /dev/null +++ b/builtin/providers/kubernetes/structures.go @@ -0,0 +1,56 @@ +package kubernetes + +import ( + "fmt" + + api "k8s.io/kubernetes/pkg/api/v1" +) + +func expandMetadata(in []interface{}) api.ObjectMeta { + meta := api.ObjectMeta{} + if len(in) < 1 { + return meta + } + m := in[0].(map[string]interface{}) + + meta.Annotations = expandStringMap(m["annotations"].(map[string]interface{})) + meta.Labels = expandStringMap(m["labels"].(map[string]interface{})) + + if v, ok := m["generate_name"]; ok { + meta.GenerateName = v.(string) + } + if v, ok := m["name"]; ok { + meta.Name = v.(string) + } + if v, ok := m["namespace"]; ok { + meta.Namespace = v.(string) + } + + return meta +} + +func expandStringMap(m map[string]interface{}) map[string]string { + result := make(map[string]string) + for k, v := range m { + result[k] = v.(string) + } + return result +} + +func flattenMetadata(meta api.ObjectMeta) []map[string]interface{} { + m := make(map[string]interface{}) + m["annotations"] = meta.Annotations + m["generate_name"] = meta.GenerateName + m["labels"] = meta.Labels + m["name"] = meta.Name + m["resource_version"] = meta.ResourceVersion + m["self_link"] = meta.SelfLink + m["uid"] = fmt.Sprintf("%v", meta.UID) + m["generation"] = meta.Generation + + if meta.Namespace != "" { + m["namespace"] = meta.Namespace + } + + return []map[string]interface{}{m} +} diff --git a/builtin/providers/kubernetes/test-infra/main.tf b/builtin/providers/kubernetes/test-infra/main.tf new file mode 100644 index 000000000..d09e4c2ca --- /dev/null +++ b/builtin/providers/kubernetes/test-infra/main.tf @@ -0,0 +1,63 @@ +provider "google" { + // Provider settings to be provided via ENV variables +} + +data "google_compute_zones" "available" {} + +resource "random_id" "cluster_name" { + byte_length = 10 +} +resource "random_id" "username" { + byte_length = 14 +} +resource "random_id" "password" { + byte_length = 16 +} + +resource "google_container_cluster" "primary" { + name = "tf-acc-test-${random_id.cluster_name.hex}" + zone = "${data.google_compute_zones.available.names[0]}" + initial_node_count = 3 + + additional_zones = [ + "${data.google_compute_zones.available.names[1]}" + ] + + master_auth { + username = "${random_id.username.hex}" + password = "${random_id.password.hex}" + } + + node_config { + oauth_scopes = [ + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring" + ] + } +} + +output "endpoint" { + value = "${google_container_cluster.primary.endpoint}" +} + +output "username" { + value = "${google_container_cluster.primary.master_auth.0.username}" +} + +output "password" { + value = "${google_container_cluster.primary.master_auth.0.password}" +} + +output "client_certificate_b64" { + value = "${google_container_cluster.primary.master_auth.0.client_certificate}" +} + +output "client_key_b64" { + value = "${google_container_cluster.primary.master_auth.0.client_key}" +} + +output "cluster_ca_certificate_b64" { + value = "${google_container_cluster.primary.master_auth.0.cluster_ca_certificate}" +} diff --git a/builtin/providers/kubernetes/validators.go b/builtin/providers/kubernetes/validators.go new file mode 100644 index 000000000..22309a34e --- /dev/null +++ b/builtin/providers/kubernetes/validators.go @@ -0,0 +1,60 @@ +package kubernetes + +import ( + "fmt" + "strings" + + apiValidation "k8s.io/kubernetes/pkg/api/validation" + utilValidation "k8s.io/kubernetes/pkg/util/validation" +) + +func validateAnnotations(value interface{}, key string) (ws []string, es []error) { + m := value.(map[string]interface{}) + for k, _ := range m { + errors := utilValidation.IsQualifiedName(strings.ToLower(k)) + if len(errors) > 0 { + for _, e := range errors { + es = append(es, fmt.Errorf("%s (%q) %s", key, k, e)) + } + } + } + return +} + +func validateName(value interface{}, key string) (ws []string, es []error) { + v := value.(string) + + errors := apiValidation.NameIsDNSLabel(v, false) + if len(errors) > 0 { + for _, err := range errors { + es = append(es, fmt.Errorf("%s %s", key, err)) + } + } + return +} + +func validateGenerateName(value interface{}, key string) (ws []string, es []error) { + v := value.(string) + + errors := apiValidation.NameIsDNSLabel(v, true) + if len(errors) > 0 { + for _, err := range errors { + es = append(es, fmt.Errorf("%s %s", key, err)) + } + } + return +} + +func validateLabels(value interface{}, key string) (ws []string, es []error) { + m := value.(map[string]interface{}) + for k, v := range m { + for _, msg := range utilValidation.IsQualifiedName(k) { + es = append(es, fmt.Errorf("%s (%q) %s", key, k, msg)) + } + val := v.(string) + for _, msg := range utilValidation.IsValidLabelValue(val) { + es = append(es, fmt.Errorf("%s (%q) %s", key, val, msg)) + } + } + return +} diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index 5032a756d..2f48908c7 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -37,6 +37,7 @@ import ( icinga2provider "github.com/hashicorp/terraform/builtin/providers/icinga2" ignitionprovider "github.com/hashicorp/terraform/builtin/providers/ignition" influxdbprovider "github.com/hashicorp/terraform/builtin/providers/influxdb" + kubernetesprovider "github.com/hashicorp/terraform/builtin/providers/kubernetes" libratoprovider "github.com/hashicorp/terraform/builtin/providers/librato" logentriesprovider "github.com/hashicorp/terraform/builtin/providers/logentries" mailgunprovider "github.com/hashicorp/terraform/builtin/providers/mailgun" @@ -112,6 +113,7 @@ var InternalProviders = map[string]plugin.ProviderFunc{ "icinga2": icinga2provider.Provider, "ignition": ignitionprovider.Provider, "influxdb": influxdbprovider.Provider, + "kubernetes": kubernetesprovider.Provider, "librato": libratoprovider.Provider, "logentries": logentriesprovider.Provider, "mailgun": mailgunprovider.Provider, diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 226d1a2fd..35f16eb60 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -40,6 +40,7 @@ body.layout-heroku, body.layout-ignition, body.layout-icinga2, body.layout-influxdb, +body.layout-kubernetes, body.layout-librato, body.layout-logentries, body.layout-mailgun, diff --git a/website/source/docs/providers/kubernetes/index.html.markdown b/website/source/docs/providers/kubernetes/index.html.markdown new file mode 100644 index 000000000..4a136de28 --- /dev/null +++ b/website/source/docs/providers/kubernetes/index.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "kubernetes" +page_title: "Provider: Kubernetes" +sidebar_current: "docs-kubernetes-index" +description: |- + The Kubernetes (K8s) provider is used to interact with the resources supported by Kubernetes. The provider needs to be configured with the proper credentials before it can be used. +--- + +# Kubernetes Provider + +The Kubernetes (K8S) provider is used to interact with the resources supported by Kubernetes. The provider needs to be configured with the proper credentials before it can be used. + +Use the navigation to the left to read about the available resources. + +-> **Note:** The Kubernetes provider is new as of Terraform 0.9. It is ready to be used but many features are still being added. If there is a Kubernetes feature missing, please report it in the GitHub repo. + +## Example Usage + +``` +provider "kubernetes" { + config_context_auth_info = "ops" + config_context_cluster = "mycluster" +} + +resource "kubernetes_namespace" "example" { + metadata { + name = "my-first-namespace" + } +} +``` + +## Authentication + +There are generally two ways to configure the Kubernetes provider. + +The provider always first tries to load **a config file** from a given +(or default) location - this requires valid `config_context_auth_info` & `config_context_cluster`. + +The other way is **statically** define all the credentials: + +``` +provider "kubernetes" { + host = "https://104.196.242.174" + username = "ClusterMaster" + password = "MindTheGap" + client_certificate = "${file("~/.kube/client-cert.pem")}" + client_key = "${file("~/.kube/client-key.pem")}" + cluster_ca_certificate = "${file("~/.kube/cluster-ca-cert.pem")}" +} +``` + +If you have **both** valid configuration in a config file and static configuration, the static one is used as override. +i.e. any static field will override its counterpart loaded from the config. + +## Argument Reference + +The following arguments are supported: + +* `host` - (Optional) The hostname (in form of URI) of Kubernetes master. Can be sourced from `KUBE_HOST`. Defaults to `https://localhost`. +* `username` - (Optional) The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint. Can be sourced from `KUBE_USER`. +* `password` - (Optional) The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint. Can be sourced from `KUBE_PASSWORD`. +* `insecure`- (Optional) Whether server should be accessed without verifying the TLS certificate. Can be sourced from `KUBE_INSECURE`. Defaults to `false`. +* `client_certificate` - (Optional) PEM-encoded client certificate for TLS authentication. Can be sourced from `KUBE_CLIENT_CERT_DATA`. +* `client_key` - (Optional) PEM-encoded client certificate key for TLS authentication. Can be sourced from `KUBE_CLIENT_KEY_DATA`. +* `cluster_ca_certificate` - (Optional) PEM-encoded root certificates bundle for TLS authentication. Can be sourced from `KUBE_CLUSTER_CA_CERT_DATA`. +* `config_path` - (Optional) Path to the kube config file. Can be sourced from `KUBE_CONFIG`. Defaults to `~/.kube/config`. +* `config_context_auth_info` - (Optional) Authentication info context of the kube config (name of the kubeconfig user, `--user` flag in `kubectl`). Can be sourced from `KUBE_CTX_AUTH_INFO`. +* `config_context_cluster` - (Optional) Cluster context of the kube config (name of the kubeconfig cluster, `--cluster` flag in `kubectl`). Can be sourced from `KUBE_CTX_CLUSTER`. diff --git a/website/source/docs/providers/kubernetes/r/namespace.html.markdown b/website/source/docs/providers/kubernetes/r/namespace.html.markdown new file mode 100644 index 000000000..c24d3e774 --- /dev/null +++ b/website/source/docs/providers/kubernetes/r/namespace.html.markdown @@ -0,0 +1,61 @@ +--- +layout: "kubernetes" +page_title: "Kubernetes: kubernetes_namespace" +sidebar_current: "docs-kubernetes-resource-namespace" +description: |- + Kubernetes supports multiple virtual clusters backed by the same physical cluster. These virtual clusters are called namespaces. +--- + +# kubernetes_namespace + +Kubernetes supports multiple virtual clusters backed by the same physical cluster. These virtual clusters are called namespaces. +Read more about namespaces at https://kubernetes.io/docs/user-guide/namespaces/ + +## Example Usage + +``` +resource "kubernetes_namespace" "example" { + metadata { + annotations { + name = "example-annotation" + } + labels { + mylabel = "label-value" + } + name = "TerraformExampleNamespace" + } +} + +``` + +## Argument Reference + +The following arguments are supported: + +* `metadata` - (Required) Standard namespace's [metadata](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#metadata). + +## Nested Blocks + +### `metadata` + +#### Arguments + +* `annotations` - (Optional) An unstructured key value map stored with the namespace that may be used to store arbitrary metadata. More info: http://kubernetes.io/docs/user-guide/annotations +* `generate_name` - (Optional) Prefix, used by the server, to generate a unique name ONLY IF the `name` field has not been provided. This value will also be combined with a unique suffix. Read more about [name idempotency](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#idempotency). +* `labels` - (Optional) Map of string keys and values that can be used to organize and categorize (scope and select) namespaces. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels +* `name` - (Optional) Name of the namespace, must be unique. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names + +#### Attributes + +* `generation` - A sequence number representing a specific generation of the desired state. +* `resource_version` - An opaque value that represents the internal version of this namespace that can be used by clients to determine when namespaces have changed. Read more about [concurrency control and consistency](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency). +* `self_link` - A URL representing this namespace. +* `uid` - The unique in time and space value for this namespace. More info: http://kubernetes.io/docs/user-guide/identifiers#uids + +## Import + +Namespaces can be imported using their name, e.g. + +``` +$ terraform import kubernetes_namespace.n TerraformExampleNamespace +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 5b7b3242a..bb9034cc3 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -303,6 +303,10 @@ InfluxDB + > + Kubernetes + + > Librato diff --git a/website/source/layouts/kubernetes.erb b/website/source/layouts/kubernetes.erb new file mode 100644 index 000000000..c7119a8ac --- /dev/null +++ b/website/source/layouts/kubernetes.erb @@ -0,0 +1,26 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> +<% end %>