diff --git a/builtin/providers/kubernetes/provider.go b/builtin/providers/kubernetes/provider.go index 4670024e3..9d0d23cc3 100644 --- a/builtin/providers/kubernetes/provider.go +++ b/builtin/providers/kubernetes/provider.go @@ -81,7 +81,8 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "kubernetes_namespace": resourceKubernetesNamespace(), + "kubernetes_config_map": resourceKubernetesConfigMap(), + "kubernetes_namespace": resourceKubernetesNamespace(), }, ConfigureFunc: providerConfigure, } diff --git a/builtin/providers/kubernetes/resource_kubernetes_config_map.go b/builtin/providers/kubernetes/resource_kubernetes_config_map.go new file mode 100644 index 000000000..460ca638e --- /dev/null +++ b/builtin/providers/kubernetes/resource_kubernetes_config_map.go @@ -0,0 +1,125 @@ +package kubernetes + +import ( + "log" + + "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 resourceKubernetesConfigMap() *schema.Resource { + return &schema.Resource{ + Create: resourceKubernetesConfigMapCreate, + Read: resourceKubernetesConfigMapRead, + Exists: resourceKubernetesConfigMapExists, + Update: resourceKubernetesConfigMapUpdate, + Delete: resourceKubernetesConfigMapDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "metadata": namespacedMetadataSchema("config map", true), + "data": { + Type: schema.TypeMap, + Description: "A map of the configuration data.", + Optional: true, + }, + }, + } +} + +func resourceKubernetesConfigMapCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + metadata := expandMetadata(d.Get("metadata").([]interface{})) + cfgMap := api.ConfigMap{ + ObjectMeta: metadata, + Data: expandStringMap(d.Get("data").(map[string]interface{})), + } + log.Printf("[INFO] Creating new config map: %#v", cfgMap) + out, err := conn.CoreV1().ConfigMaps(metadata.Namespace).Create(&cfgMap) + if err != nil { + return err + } + log.Printf("[INFO] Submitted new config map: %#v", out) + d.SetId(buildId(out.ObjectMeta)) + + return resourceKubernetesConfigMapRead(d, meta) +} + +func resourceKubernetesConfigMapRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + namespace, name := idParts(d.Id()) + log.Printf("[INFO] Reading config map %s", name) + cfgMap, err := conn.CoreV1().ConfigMaps(namespace).Get(name) + if err != nil { + log.Printf("[DEBUG] Received error: %#v", err) + return err + } + log.Printf("[INFO] Received config map: %#v", cfgMap) + err = d.Set("metadata", flattenMetadata(cfgMap.ObjectMeta)) + if err != nil { + return err + } + d.Set("data", cfgMap.Data) + + return nil +} + +func resourceKubernetesConfigMapUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + metadata := expandMetadata(d.Get("metadata").([]interface{})) + namespace, name := idParts(d.Id()) + // This is necessary in case the name is generated + metadata.Name = name + + cfgMap := api.ConfigMap{ + ObjectMeta: metadata, + Data: expandStringMap(d.Get("data").(map[string]interface{})), + } + log.Printf("[INFO] Updating config map: %#v", cfgMap) + out, err := conn.CoreV1().ConfigMaps(namespace).Update(&cfgMap) + if err != nil { + return err + } + log.Printf("[INFO] Submitted updated config map: %#v", out) + d.SetId(buildId(out.ObjectMeta)) + + return resourceKubernetesConfigMapRead(d, meta) +} + +func resourceKubernetesConfigMapDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + namespace, name := idParts(d.Id()) + log.Printf("[INFO] Deleting config map: %#v", name) + err := conn.CoreV1().ConfigMaps(namespace).Delete(name, &api.DeleteOptions{}) + if err != nil { + return err + } + + log.Printf("[INFO] Config map %s deleted", name) + + d.SetId("") + return nil +} + +func resourceKubernetesConfigMapExists(d *schema.ResourceData, meta interface{}) (bool, error) { + conn := meta.(*kubernetes.Clientset) + + namespace, name := idParts(d.Id()) + log.Printf("[INFO] Checking config map %s", name) + _, err := conn.CoreV1().ConfigMaps(namespace).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) + } + return true, err +} diff --git a/builtin/providers/kubernetes/resource_kubernetes_config_map_test.go b/builtin/providers/kubernetes/resource_kubernetes_config_map_test.go new file mode 100644 index 000000000..e3d0e5097 --- /dev/null +++ b/builtin/providers/kubernetes/resource_kubernetes_config_map_test.go @@ -0,0 +1,284 @@ +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 TestAccKubernetesConfigMap_basic(t *testing.T) { + var conf api.ConfigMap + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "kubernetes_config_map.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesConfigMapDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesConfigMapConfig_basic(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesConfigMapExists("kubernetes_config_map.test", &conf), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.annotations.%", "2"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.annotations.TestAnnotationOne", "one"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.annotations.TestAnnotationTwo", "two"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{"TestAnnotationOne": "one", "TestAnnotationTwo": "two"}), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.labels.%", "3"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.labels.TestLabelOne", "one"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.labels.TestLabelTwo", "two"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.labels.TestLabelThree", "three"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{"TestLabelOne": "one", "TestLabelTwo": "two", "TestLabelThree": "three"}), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.name", name), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.uid"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "data.%", "2"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "data.one", "first"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "data.two", "second"), + testAccCheckConfigMapData(&conf, map[string]string{"one": "first", "two": "second"}), + ), + }, + { + Config: testAccKubernetesConfigMapConfig_modified(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesConfigMapExists("kubernetes_config_map.test", &conf), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.annotations.%", "2"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.annotations.TestAnnotationOne", "one"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.annotations.Different", "1234"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{"TestAnnotationOne": "one", "Different": "1234"}), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.labels.%", "2"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.labels.TestLabelOne", "one"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.labels.TestLabelThree", "three"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{"TestLabelOne": "one", "TestLabelThree": "three"}), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.name", name), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.uid"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "data.%", "3"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "data.one", "first"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "data.two", "second"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "data.nine", "ninth"), + testAccCheckConfigMapData(&conf, map[string]string{"one": "first", "two": "second", "nine": "ninth"}), + ), + }, + { + Config: testAccKubernetesConfigMapConfig_noData(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesConfigMapExists("kubernetes_config_map.test", &conf), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.annotations.%", "0"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.labels.%", "0"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.name", name), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.uid"), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "data.%", "0"), + testAccCheckConfigMapData(&conf, map[string]string{}), + ), + }, + }, + }) +} + +func TestAccKubernetesConfigMap_importBasic(t *testing.T) { + resourceName := "kubernetes_config_map.test" + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesConfigMapDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesConfigMapConfig_basic(name), + }, + + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccKubernetesConfigMap_generatedName(t *testing.T) { + var conf api.ConfigMap + prefix := "tf-acc-test-gen-" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "kubernetes_config_map.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesConfigMapDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesConfigMapConfig_generatedName(prefix), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesConfigMapExists("kubernetes_config_map.test", &conf), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.annotations.%", "0"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.labels.%", "0"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_config_map.test", "metadata.0.generate_name", prefix), + resource.TestMatchResourceAttr("kubernetes_config_map.test", "metadata.0.name", regexp.MustCompile("^"+prefix)), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_config_map.test", "metadata.0.uid"), + ), + }, + }, + }) +} + +func TestAccKubernetesConfigMap_importGeneratedName(t *testing.T) { + resourceName := "kubernetes_config_map.test" + prefix := "tf-acc-test-gen-import-" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesConfigMapDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesConfigMapConfig_generatedName(prefix), + }, + + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckConfigMapData(m *api.ConfigMap, expected map[string]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if len(expected) == 0 && len(m.Data) == 0 { + return nil + } + if !reflect.DeepEqual(m.Data, expected) { + return fmt.Errorf("%s data don't match.\nExpected: %q\nGiven: %q", + m.Name, expected, m.Data) + } + return nil + } +} + +func testAccCheckKubernetesConfigMapDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*kubernetes.Clientset) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "kubernetes_config_map" { + continue + } + namespace, name := idParts(rs.Primary.ID) + resp, err := conn.CoreV1().ConfigMaps(namespace).Get(name) + if err == nil { + if resp.Name == rs.Primary.ID { + return fmt.Errorf("Config Map still exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +func testAccCheckKubernetesConfigMapExists(n string, obj *api.ConfigMap) 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) + namespace, name := idParts(rs.Primary.ID) + out, err := conn.CoreV1().ConfigMaps(namespace).Get(name) + if err != nil { + return err + } + + *obj = *out + return nil + } +} + +func testAccKubernetesConfigMapConfig_basic(name string) string { + return fmt.Sprintf(` +resource "kubernetes_config_map" "test" { + metadata { + annotations { + TestAnnotationOne = "one" + TestAnnotationTwo = "two" + } + labels { + TestLabelOne = "one" + TestLabelTwo = "two" + TestLabelThree = "three" + } + name = "%s" + } + data { + one = "first" + two = "second" + } +}`, name) +} + +func testAccKubernetesConfigMapConfig_modified(name string) string { + return fmt.Sprintf(` +resource "kubernetes_config_map" "test" { + metadata { + annotations { + TestAnnotationOne = "one" + Different = "1234" + } + labels { + TestLabelOne = "one" + TestLabelThree = "three" + } + name = "%s" + } + data { + one = "first" + two = "second" + nine = "ninth" + } +}`, name) +} + +func testAccKubernetesConfigMapConfig_noData(name string) string { + return fmt.Sprintf(` +resource "kubernetes_config_map" "test" { + metadata { + name = "%s" + } +}`, name) +} + +func testAccKubernetesConfigMapConfig_generatedName(prefix string) string { + return fmt.Sprintf(` +resource "kubernetes_config_map" "test" { + metadata { + generate_name = "%s" + } + data { + one = "first" + two = "second" + } +}`, prefix) +} diff --git a/builtin/providers/kubernetes/schema_metadata.go b/builtin/providers/kubernetes/schema_metadata.go index 8a3b3efaf..27644f83a 100644 --- a/builtin/providers/kubernetes/schema_metadata.go +++ b/builtin/providers/kubernetes/schema_metadata.go @@ -73,3 +73,34 @@ func metadataSchema(objectName string) *schema.Schema { }, } } + +func namespacedMetadataSchema(objectName string, generatableName bool) *schema.Schema { + fields := metadataFields(objectName) + fields["namespace"] = &schema.Schema{ + Type: schema.TypeString, + Description: fmt.Sprintf("Namespace defines the space within which name of the %s must be unique.", objectName), + Optional: true, + ForceNew: true, + Default: "default", + } + if generatableName { + 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 index 5cf894376..8b98cee32 100644 --- a/builtin/providers/kubernetes/structures.go +++ b/builtin/providers/kubernetes/structures.go @@ -2,10 +2,20 @@ package kubernetes import ( "fmt" + "strings" api "k8s.io/kubernetes/pkg/api/v1" ) +func idParts(id string) (string, string) { + parts := strings.Split(id, "/") + return parts[0], parts[1] +} + +func buildId(meta api.ObjectMeta) string { + return meta.Namespace + "/" + meta.Name +} + func expandMetadata(in []interface{}) api.ObjectMeta { meta := api.ObjectMeta{} if len(in) < 1 { diff --git a/website/source/docs/providers/kubernetes/r/config_map.html.markdown b/website/source/docs/providers/kubernetes/r/config_map.html.markdown new file mode 100644 index 000000000..a1b4ea962 --- /dev/null +++ b/website/source/docs/providers/kubernetes/r/config_map.html.markdown @@ -0,0 +1,60 @@ +--- +layout: "kubernetes" +page_title: "Kubernetes: kubernetes_config_map" +sidebar_current: "docs-kubernetes-resource-config-map" +description: |- + The resource provides mechanisms to inject containers with configuration data while keeping containers agnostic of Kubernetes. +--- + +# kubernetes_config_map + +The resource provides mechanisms to inject containers with configuration data while keeping containers agnostic of Kubernetes. +Config Map can be used to store fine-grained information like individual properties or coarse-grained information like entire config files or JSON blobs. + +## Example Usage + +``` +resource "kubernetes_config_map" "example" { + metadata { + name = "my_config" + } + data { + api_host = "myhost:443" + db_host = "dbhost:5432" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `data` - (Optional) A map of the configuration data. +* `metadata` - (Required) Standard config map's metadata. More info: 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 config map 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: 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) the config map. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels +* `name` - (Optional) Name of the config map, must be unique. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names +* `namespace` - (Optional) Namespace defines the space within which name of the config map must be unique. + +#### 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 config map that can be used by clients to determine when config map has changed. Read more: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#concurrency-control-and-consistency +* `self_link` - A URL representing this config map. +* `uid` - The unique in time and space value for this config map. More info: http://kubernetes.io/docs/user-guide/identifiers#uids + +## Import + +Config Map can be imported using its name, e.g. + +``` +$ terraform import kubernetes_config_map.example my_config +``` diff --git a/website/source/layouts/kubernetes.erb b/website/source/layouts/kubernetes.erb index c7119a8ac..147bccbf4 100644 --- a/website/source/layouts/kubernetes.erb +++ b/website/source/layouts/kubernetes.erb @@ -13,6 +13,9 @@ > Resources