diff --git a/builtin/providers/kubernetes/provider.go b/builtin/providers/kubernetes/provider.go index 8a613ab13..5e57a5258 100644 --- a/builtin/providers/kubernetes/provider.go +++ b/builtin/providers/kubernetes/provider.go @@ -90,6 +90,7 @@ func Provider() terraform.ResourceProvider { "kubernetes_namespace": resourceKubernetesNamespace(), "kubernetes_persistent_volume": resourceKubernetesPersistentVolume(), "kubernetes_persistent_volume_claim": resourceKubernetesPersistentVolumeClaim(), + "kubernetes_resource_quota": resourceKubernetesResourceQuota(), "kubernetes_secret": resourceKubernetesSecret(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/kubernetes/resource_kubernetes_resource_quota.go b/builtin/providers/kubernetes/resource_kubernetes_resource_quota.go new file mode 100644 index 000000000..758cf8f4f --- /dev/null +++ b/builtin/providers/kubernetes/resource_kubernetes_resource_quota.go @@ -0,0 +1,211 @@ +package kubernetes + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + pkgApi "k8s.io/kubernetes/pkg/api" + "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 resourceKubernetesResourceQuota() *schema.Resource { + return &schema.Resource{ + Create: resourceKubernetesResourceQuotaCreate, + Read: resourceKubernetesResourceQuotaRead, + Exists: resourceKubernetesResourceQuotaExists, + Update: resourceKubernetesResourceQuotaUpdate, + Delete: resourceKubernetesResourceQuotaDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "metadata": namespacedMetadataSchema("resource quota", true), + "spec": { + Type: schema.TypeList, + Description: "Spec defines the desired quota. http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "hard": { + Type: schema.TypeMap, + Description: "The set of desired hard limits for each named resource. More info: http://releases.k8s.io/HEAD/docs/design/admission_control_resource_quota.md#admissioncontrol-plugin-resourcequota", + Optional: true, + Elem: schema.TypeString, + ValidateFunc: validateResourceList, + }, + "scopes": { + Type: schema.TypeSet, + Description: "A collection of filters that must match each object tracked by a quota. If not specified, the quota matches all objects.", + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + }, + }, + }, + } +} + +func resourceKubernetesResourceQuotaCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + metadata := expandMetadata(d.Get("metadata").([]interface{})) + spec, err := expandResourceQuotaSpec(d.Get("spec").([]interface{})) + if err != nil { + return err + } + resQuota := api.ResourceQuota{ + ObjectMeta: metadata, + Spec: spec, + } + log.Printf("[INFO] Creating new resource quota: %#v", resQuota) + out, err := conn.CoreV1().ResourceQuotas(metadata.Namespace).Create(&resQuota) + if err != nil { + return fmt.Errorf("Failed to create resource quota: %s", err) + } + log.Printf("[INFO] Submitted new resource quota: %#v", out) + d.SetId(buildId(out.ObjectMeta)) + + err = resource.Retry(1*time.Minute, func() *resource.RetryError { + quota, err := conn.CoreV1().ResourceQuotas(out.Namespace).Get(out.Name) + if err != nil { + return resource.NonRetryableError(err) + } + if resourceListEquals(spec.Hard, quota.Status.Hard) { + return nil + } + err = fmt.Errorf("Quotas don't match after creation.\nExpected: %#v\nGiven: %#v", + spec.Hard, quota.Status.Hard) + return resource.RetryableError(err) + }) + if err != nil { + return err + } + + return resourceKubernetesResourceQuotaRead(d, meta) +} + +func resourceKubernetesResourceQuotaRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + namespace, name := idParts(d.Id()) + log.Printf("[INFO] Reading resource quota %s", name) + resQuota, err := conn.CoreV1().ResourceQuotas(namespace).Get(name) + if err != nil { + log.Printf("[DEBUG] Received error: %#v", err) + return err + } + log.Printf("[INFO] Received resource quota: %#v", resQuota) + + // This is to work around K8S bug + // See https://github.com/kubernetes/kubernetes/issues/44539 + if resQuota.ObjectMeta.GenerateName == "" { + if v, ok := d.GetOk("metadata.0.generate_name"); ok { + resQuota.ObjectMeta.GenerateName = v.(string) + } + } + + err = d.Set("metadata", flattenMetadata(resQuota.ObjectMeta)) + if err != nil { + return err + } + err = d.Set("spec", flattenResourceQuotaSpec(resQuota.Spec)) + if err != nil { + return err + } + + return nil +} + +func resourceKubernetesResourceQuotaUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + namespace, name := idParts(d.Id()) + + ops := patchMetadata("metadata.0.", "/metadata/", d) + var spec api.ResourceQuotaSpec + waitForChangedSpec := false + if d.HasChange("spec") { + var err error + spec, err = expandResourceQuotaSpec(d.Get("spec").([]interface{})) + if err != nil { + return err + } + ops = append(ops, &ReplaceOperation{ + Path: "/spec", + Value: spec, + }) + waitForChangedSpec = true + } + data, err := ops.MarshalJSON() + if err != nil { + return fmt.Errorf("Failed to marshal update operations: %s", err) + } + log.Printf("[INFO] Updating resource quota %q: %v", name, string(data)) + out, err := conn.CoreV1().ResourceQuotas(namespace).Patch(name, pkgApi.JSONPatchType, data) + if err != nil { + return fmt.Errorf("Failed to update resource quota: %s", err) + } + log.Printf("[INFO] Submitted updated resource quota: %#v", out) + d.SetId(buildId(out.ObjectMeta)) + + if waitForChangedSpec { + err = resource.Retry(1*time.Minute, func() *resource.RetryError { + quota, err := conn.CoreV1().ResourceQuotas(namespace).Get(name) + if err != nil { + return resource.NonRetryableError(err) + } + if resourceListEquals(spec.Hard, quota.Status.Hard) { + return nil + } + err = fmt.Errorf("Quotas don't match after update.\nExpected: %#v\nGiven: %#v", + spec.Hard, quota.Status.Hard) + return resource.RetryableError(err) + }) + if err != nil { + return err + } + } + + return resourceKubernetesResourceQuotaRead(d, meta) +} + +func resourceKubernetesResourceQuotaDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*kubernetes.Clientset) + + namespace, name := idParts(d.Id()) + log.Printf("[INFO] Deleting resource quota: %#v", name) + err := conn.CoreV1().ResourceQuotas(namespace).Delete(name, &api.DeleteOptions{}) + if err != nil { + return err + } + + log.Printf("[INFO] Resource quota %s deleted", name) + + d.SetId("") + return nil +} + +func resourceKubernetesResourceQuotaExists(d *schema.ResourceData, meta interface{}) (bool, error) { + conn := meta.(*kubernetes.Clientset) + + namespace, name := idParts(d.Id()) + log.Printf("[INFO] Checking resource quota %s", name) + _, err := conn.CoreV1().ResourceQuotas(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_resource_quota_test.go b/builtin/providers/kubernetes/resource_kubernetes_resource_quota_test.go new file mode 100644 index 000000000..5935f7ea5 --- /dev/null +++ b/builtin/providers/kubernetes/resource_kubernetes_resource_quota_test.go @@ -0,0 +1,352 @@ +package kubernetes + +import ( + "fmt" + "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 TestAccKubernetesResourceQuota_basic(t *testing.T) { + var conf api.ResourceQuota + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "kubernetes_resource_quota.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesResourceQuotaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesResourceQuotaConfig_basic(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesResourceQuotaExists("kubernetes_resource_quota.test", &conf), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.annotations.%", "1"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.annotations.TestAnnotationOne", "one"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{"TestAnnotationOne": "one"}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.%", "3"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.TestLabelOne", "one"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.TestLabelThree", "three"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.TestLabelFour", "four"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{"TestLabelOne": "one", "TestLabelThree": "three", "TestLabelFour": "four"}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.name", name), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.uid"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.%", "3"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.limits.cpu", "2"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.limits.memory", "2Gi"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.pods", "4"), + ), + }, + { + Config: testAccKubernetesResourceQuotaConfig_metaModified(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesResourceQuotaExists("kubernetes_resource_quota.test", &conf), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.annotations.%", "2"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.annotations.TestAnnotationOne", "one"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.annotations.TestAnnotationTwo", "two"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{"TestAnnotationOne": "one", "TestAnnotationTwo": "two"}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.%", "3"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.TestLabelOne", "one"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.TestLabelTwo", "two"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.TestLabelThree", "three"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{"TestLabelOne": "one", "TestLabelTwo": "two", "TestLabelThree": "three"}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.name", name), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.uid"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.%", "3"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.limits.cpu", "2"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.limits.memory", "2Gi"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.pods", "4"), + ), + }, + { + Config: testAccKubernetesResourceQuotaConfig_specModified(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesResourceQuotaExists("kubernetes_resource_quota.test", &conf), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.annotations.%", "0"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.%", "0"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.name", name), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.uid"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.%", "4"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.limits.cpu", "4"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.requests.cpu", "1"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.limits.memory", "4Gi"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.pods", "10"), + ), + }, + }, + }) +} + +func TestAccKubernetesResourceQuota_generatedName(t *testing.T) { + var conf api.ResourceQuota + prefix := "tf-acc-test-" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "kubernetes_resource_quota.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesResourceQuotaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesResourceQuotaConfig_generatedName(prefix), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesResourceQuotaExists("kubernetes_resource_quota.test", &conf), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.annotations.%", "0"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.%", "0"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.generate_name", prefix), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.uid"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.%", "1"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.pods", "10"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.scopes.#", "0"), + ), + }, + }, + }) +} + +func TestAccKubernetesResourceQuota_withScopes(t *testing.T) { + var conf api.ResourceQuota + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "kubernetes_resource_quota.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesResourceQuotaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesResourceQuotaConfig_withScopes(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesResourceQuotaExists("kubernetes_resource_quota.test", &conf), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.annotations.%", "0"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.%", "0"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.name", name), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.uid"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.%", "1"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.pods", "10"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.scopes.#", "1"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.scopes.193563370", "BestEffort"), + ), + }, + { + Config: testAccKubernetesResourceQuotaConfig_withScopesModified(name), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKubernetesResourceQuotaExists("kubernetes_resource_quota.test", &conf), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.annotations.%", "0"), + testAccCheckMetaAnnotations(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.labels.%", "0"), + testAccCheckMetaLabels(&conf.ObjectMeta, map[string]string{}), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "metadata.0.name", name), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.generation"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.resource_version"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.self_link"), + resource.TestCheckResourceAttrSet("kubernetes_resource_quota.test", "metadata.0.uid"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.%", "1"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.hard.pods", "10"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.scopes.#", "1"), + resource.TestCheckResourceAttr("kubernetes_resource_quota.test", "spec.0.scopes.3022121741", "NotBestEffort"), + ), + }, + }, + }) +} + +func TestAccKubernetesResourceQuota_importBasic(t *testing.T) { + resourceName := "kubernetes_resource_quota.test" + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesResourceQuotaDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesResourceQuotaConfig_basic(name), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckKubernetesResourceQuotaDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*kubernetes.Clientset) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "kubernetes_resource_quota" { + continue + } + namespace, name := idParts(rs.Primary.ID) + resp, err := conn.CoreV1().ResourceQuotas(namespace).Get(name) + if err == nil { + if resp.Namespace == namespace && resp.Name == name { + return fmt.Errorf("Resource Quota still exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +func testAccCheckKubernetesResourceQuotaExists(n string, obj *api.ResourceQuota) 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().ResourceQuotas(namespace).Get(name) + if err != nil { + return err + } + + *obj = *out + return nil + } +} + +func testAccKubernetesResourceQuotaConfig_basic(name string) string { + return fmt.Sprintf(` +resource "kubernetes_resource_quota" "test" { + metadata { + annotations { + TestAnnotationOne = "one" + } + labels { + TestLabelOne = "one" + TestLabelThree = "three" + TestLabelFour = "four" + } + name = "%s" + } + spec { + hard { + "limits.cpu" = 2 + "limits.memory" = "2Gi" + pods = 4 + } + } +} +`, name) +} + +func testAccKubernetesResourceQuotaConfig_metaModified(name string) string { + return fmt.Sprintf(` +resource "kubernetes_resource_quota" "test" { + metadata { + annotations { + TestAnnotationOne = "one" + TestAnnotationTwo = "two" + } + labels { + TestLabelOne = "one" + TestLabelTwo = "two" + TestLabelThree = "three" + } + name = "%s" + } + spec { + hard { + "limits.cpu" = 2 + "limits.memory" = "2Gi" + pods = 4 + } + } +} +`, name) +} + +func testAccKubernetesResourceQuotaConfig_specModified(name string) string { + return fmt.Sprintf(` +resource "kubernetes_resource_quota" "test" { + metadata { + name = "%s" + } + spec { + hard { + "limits.cpu" = 4 + "requests.cpu" = 1 + "limits.memory" = "4Gi" + pods = 10 + } + } +} +`, name) +} + +func testAccKubernetesResourceQuotaConfig_generatedName(prefix string) string { + return fmt.Sprintf(` +resource "kubernetes_resource_quota" "test" { + metadata { + generate_name = "%s" + } + spec { + hard { + pods = 10 + } + } +} +`, prefix) +} + +func testAccKubernetesResourceQuotaConfig_withScopes(name string) string { + return fmt.Sprintf(` +resource "kubernetes_resource_quota" "test" { + metadata { + name = "%s" + } + spec { + hard { + pods = 10 + } + scopes = ["BestEffort"] + } +} +`, name) +} + +func testAccKubernetesResourceQuotaConfig_withScopesModified(name string) string { + return fmt.Sprintf(` +resource "kubernetes_resource_quota" "test" { + metadata { + name = "%s" + } + spec { + hard { + pods = 10 + } + scopes = ["NotBestEffort"] + } +} +`, name) +} diff --git a/builtin/providers/kubernetes/structures.go b/builtin/providers/kubernetes/structures.go index 19b73aabd..992f22b63 100644 --- a/builtin/providers/kubernetes/structures.go +++ b/builtin/providers/kubernetes/structures.go @@ -163,11 +163,21 @@ func flattenResourceList(l api.ResourceList) map[string]string { func expandMapToResourceList(m map[string]interface{}) (api.ResourceList, error) { out := make(map[api.ResourceName]resource.Quantity) - for stringKey, v := range m { + for stringKey, origValue := range m { key := api.ResourceName(stringKey) - value, err := resource.ParseQuantity(v.(string)) - if err != nil { - return out, err + var value resource.Quantity + + if v, ok := origValue.(int); ok { + q := resource.NewQuantity(int64(v), resource.DecimalExponent) + value = *q + } else if v, ok := origValue.(string); ok { + var err error + value, err = resource.ParseQuantity(v) + if err != nil { + return out, err + } + } else { + return out, fmt.Errorf("Unexpected value type: %#v", origValue) } out[key] = value @@ -191,6 +201,55 @@ func expandPersistentVolumeAccessModes(s []interface{}) []api.PersistentVolumeAc return out } +func flattenResourceQuotaSpec(in api.ResourceQuotaSpec) []interface{} { + out := make([]interface{}, 1) + + m := make(map[string]interface{}, 0) + m["hard"] = flattenResourceList(in.Hard) + m["scopes"] = flattenResourceQuotaScopes(in.Scopes) + + out[0] = m + return out +} + +func expandResourceQuotaSpec(s []interface{}) (api.ResourceQuotaSpec, error) { + out := api.ResourceQuotaSpec{} + if len(s) < 1 { + return out, nil + } + m := s[0].(map[string]interface{}) + + if v, ok := m["hard"]; ok { + list, err := expandMapToResourceList(v.(map[string]interface{})) + if err != nil { + return out, err + } + out.Hard = list + } + + if v, ok := m["scopes"]; ok { + out.Scopes = expandResourceQuotaScopes(v.(*schema.Set).List()) + } + + return out, nil +} + +func flattenResourceQuotaScopes(in []api.ResourceQuotaScope) *schema.Set { + out := make([]string, len(in), len(in)) + for i, scope := range in { + out[i] = string(scope) + } + return newStringSet(schema.HashString, out) +} + +func expandResourceQuotaScopes(s []interface{}) []api.ResourceQuotaScope { + out := make([]api.ResourceQuotaScope, len(s), len(s)) + for i, scope := range s { + out[i] = api.ResourceQuotaScope(scope.(string)) + } + return out +} + func newStringSet(f schema.SchemaSetFunc, in []string) *schema.Set { var out = make([]interface{}, len(in), len(in)) for i, v := range in { @@ -198,3 +257,25 @@ func newStringSet(f schema.SchemaSetFunc, in []string) *schema.Set { } return schema.NewSet(f, out) } + +func resourceListEquals(x, y api.ResourceList) bool { + for k, v := range x { + yValue, ok := y[k] + if !ok { + return false + } + if v.Cmp(yValue) != 0 { + return false + } + } + for k, v := range y { + xValue, ok := x[k] + if !ok { + return false + } + if v.Cmp(xValue) != 0 { + return false + } + } + return true +} diff --git a/builtin/providers/kubernetes/validators.go b/builtin/providers/kubernetes/validators.go index f1dde2029..4fe12328b 100644 --- a/builtin/providers/kubernetes/validators.go +++ b/builtin/providers/kubernetes/validators.go @@ -62,12 +62,21 @@ func validateLabels(value interface{}, key string) (ws []string, es []error) { func validateResourceList(value interface{}, key string) (ws []string, es []error) { m := value.(map[string]interface{}) - for k, v := range m { - val := v.(string) - _, err := resource.ParseQuantity(val) - if err != nil { - es = append(es, fmt.Errorf("%s.%s (%q): %s", key, k, val, err)) + for k, value := range m { + if _, ok := value.(int); ok { + continue } + + if v, ok := value.(string); ok { + _, err := resource.ParseQuantity(v) + if err != nil { + es = append(es, fmt.Errorf("%s.%s (%q): %s", key, k, v, err)) + } + continue + } + + err := "Value can be either string or int" + es = append(es, fmt.Errorf("%s.%s (%#v): %s", key, k, value, err)) } return } diff --git a/website/source/docs/providers/kubernetes/r/resource_quota.html.markdown b/website/source/docs/providers/kubernetes/r/resource_quota.html.markdown new file mode 100644 index 000000000..64982144e --- /dev/null +++ b/website/source/docs/providers/kubernetes/r/resource_quota.html.markdown @@ -0,0 +1,69 @@ +--- +layout: "kubernetes" +page_title: "Kubernetes: kubernetes_resource_quota" +sidebar_current: "docs-kubernetes-resource-resource-quota" +description: |- + A resource quota provides constraints that limit aggregate resource consumption per namespace. It can limit the quantity of objects that can be created in a namespace by type, as well as the total amount of compute resources that may be consumed by resources in that project. +--- + +# kubernetes_resource_quota + +A resource quota provides constraints that limit aggregate resource consumption per namespace. It can limit the quantity of objects that can be created in a namespace by type, as well as the total amount of compute resources that may be consumed by resources in that project. + + +## Example Usage + +```hcl +resource "kubernetes_resource_quota" "example" { + metadata { + name = "terraform-example" + } + spec { + hard { + pods = 10 + } + scopes = ["BestEffort"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `metadata` - (Required) Standard resource quota's metadata. More info: https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#metadata +* `spec` - (Optional) Spec defines the desired quota. http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status + +## Nested Blocks + +### `metadata` + +#### Arguments + +* `annotations` - (Optional) An unstructured key value map stored with the resource quota that may be used to store arbitrary metadata. More info: http://kubernetes.io/docs/user-guide/annotations +* `labels` - (Optional) Map of string keys and values that can be used to organize and categorize (scope and select) the resource quota. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels +* `name` - (Optional) Name of the resource quota, 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 resource quota 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 resource quota that can be used by clients to determine when resource quota 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 resource quota. +* `uid` - The unique in time and space value for this resource quota. More info: http://kubernetes.io/docs/user-guide/identifiers#uids + +### `spec` + +#### Arguments + +* `hard` - (Optional) The set of desired hard limits for each named resource. More info: http://releases.k8s.io/HEAD/docs/design/admission_control_resource_quota.md#admissioncontrol-plugin-resourcequota +* `scopes` - (Optional) A collection of filters that must match each object tracked by a quota. If not specified, the quota matches all objects. + +## Import + +Resource Quota can be imported using its name, e.g. + +``` +$ terraform import kubernetes_resource_quota.example terraform-example +``` \ No newline at end of file diff --git a/website/source/layouts/kubernetes.erb b/website/source/layouts/kubernetes.erb index 2dda81b85..96a8fbaee 100644 --- a/website/source/layouts/kubernetes.erb +++ b/website/source/layouts/kubernetes.erb @@ -25,9 +25,12 @@ > kubernetes_persistent_volume_claim + > + kubernetes_resource_quota + > - kubernetes_secret - + kubernetes_secret +