From fd292ca1a38089cfaebe1493870e57b7b907e2de Mon Sep 17 00:00:00 2001 From: Mauricio Alvarez Leon <65101411+BBBmau@users.noreply.github.com> Date: Tue, 11 Oct 2022 09:04:50 -0700 Subject: [PATCH] Add resource_kubernetes_env (#1838) * initial resource commit * add basic CRUD functions * add metadata block and create logic * add update function * add force attribute * add read function & getEnvs function * add field_manager attribute * fix read function * Add read function * Add Delete function * draft createEnv function in test * complete draft kubernetes_env_test.go * remove unnecessary getEnvs function * Finish Test for kubernetes_env_test * Add kubernetes_env docs * add value_from attribute to tests * add value_from to schema * Add Flatten Function * Update kubernetenv_test * Add changelog-entry * Format Env Docs * Fix test format * Update website/docs/r/env.html.markdown Co-authored-by: John Houston * Last Formatting Steps * Update structures_container.go * Add changelog-entry * Delete extra changelog * Add missing info * Add changelog-entry * Description Change Co-authored-by: John Houston --- .changelog/1838.txt | 3 + kubernetes/provider.go | 1 + kubernetes/resource_kubernetes_env.go | 462 +++++++++++++++++++++ kubernetes/resource_kubernetes_env_test.go | 274 ++++++++++++ kubernetes/structures_env.go | 190 +++++++++ website/docs/r/env.html.markdown | 109 +++++ 6 files changed, 1039 insertions(+) create mode 100644 .changelog/1838.txt create mode 100644 kubernetes/resource_kubernetes_env.go create mode 100644 kubernetes/resource_kubernetes_env_test.go create mode 100644 kubernetes/structures_env.go create mode 100644 website/docs/r/env.html.markdown diff --git a/.changelog/1838.txt b/.changelog/1838.txt new file mode 100644 index 0000000000..621058694e --- /dev/null +++ b/.changelog/1838.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +Add resource_kubernetes_env.go +``` diff --git a/kubernetes/provider.go b/kubernetes/provider.go index 912262c4de..eb4dc90c0e 100644 --- a/kubernetes/provider.go +++ b/kubernetes/provider.go @@ -253,6 +253,7 @@ func Provider() *schema.Provider { "kubernetes_pod_v1": resourceKubernetesPod(), "kubernetes_endpoints": resourceKubernetesEndpoints(), "kubernetes_endpoints_v1": resourceKubernetesEndpoints(), + "kubernetes_env": resourceKubernetesEnv(), "kubernetes_limit_range": resourceKubernetesLimitRange(), "kubernetes_limit_range_v1": resourceKubernetesLimitRange(), "kubernetes_persistent_volume": resourceKubernetesPersistentVolume(), diff --git a/kubernetes/resource_kubernetes_env.go b/kubernetes/resource_kubernetes_env.go new file mode 100644 index 0000000000..e07c33b833 --- /dev/null +++ b/kubernetes/resource_kubernetes_env.go @@ -0,0 +1,462 @@ +package kubernetes + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-kubernetes/util" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/types" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8sschema "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/restmapper" +) + +func resourceKubernetesEnv() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceKubernetesEnvCreate, + ReadContext: resourceKubernetesEnvRead, + UpdateContext: resourceKubernetesEnvUpdate, + DeleteContext: resourceKubernetesEnvDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "metadata": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "The name of the resource.", + Required: true, + ForceNew: true, + }, + "namespace": { + Type: schema.TypeString, + Description: "The namespace of the resource.", + Optional: true, + ForceNew: true, + }, + }, + }, + }, + "container": { + Type: schema.TypeString, + Description: "Name of the container for which we are updating the environment variables.", + Required: true, + }, + "api_version": { + Type: schema.TypeString, + Description: "API Version of Field Manager", + Required: true, + }, + "kind": { + Type: schema.TypeString, + Description: "Type of resource being used", + Required: true, + }, + "env": { + Type: schema.TypeList, + Description: "List of custom values used to represent environment variables", + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the environment variable. Must be a C_IDENTIFIER", + }, + "value": { + Type: schema.TypeString, + Optional: true, + Description: `Variable references $(VAR_NAME) are expanded using the previous defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. The $(VAR_NAME) syntax can be escaped with a double $$, ie: $$(VAR_NAME). Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".`, + }, + "value_from": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Source for the environment variable's value", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "config_map_key_ref": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Selects a key of a ConfigMap.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Optional: true, + Description: "The key to select.", + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#names", + }, + "optional": { + Type: schema.TypeBool, + Optional: true, + Description: "Specify whether the ConfigMap or its key must be defined.", + }, + }, + }, + }, + "field_ref": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Selects a field of the pod: supports metadata.name, metadata.namespace, metadata.labels, metadata.annotations, spec.nodeName, spec.serviceAccountName, status.podIP.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "api_version": { + Type: schema.TypeString, + Optional: true, + Default: "v1", + Description: `Version of the schema the FieldPath is written in terms of, defaults to "v1".`, + }, + "field_path": { + Type: schema.TypeString, + Optional: true, + Description: "Path of the field to select in the specified API version", + }, + }, + }, + }, + "resource_field_ref": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "container_name": { + Type: schema.TypeString, + Optional: true, + }, + "divisor": { + Type: schema.TypeString, + Optional: true, + Default: "1", + ValidateFunc: validateResourceQuantity, + DiffSuppressFunc: suppressEquivalentResourceQuantity, + }, + "resource": { + Type: schema.TypeString, + Required: true, + Description: "Resource to select", + }, + }, + }, + }, + "secret_key_ref": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Selects a key of a secret in the pod's namespace.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Optional: true, + Description: "The key of the secret to select from. Must be a valid secret key.", + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#names", + }, + "optional": { + Type: schema.TypeBool, + Optional: true, + Description: "Specify whether the Secret or its key must be defined.", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "force": { + Type: schema.TypeBool, + Description: "Force overwriting environments that were created or edited outside of Terraform.", + Optional: true, + }, + "field_manager": { + Type: schema.TypeString, + Description: "Set the name of the field manager for the specified environment variables.", + Optional: true, + Default: defaultFieldManagerName, + }, + }, + } +} + +func resourceKubernetesEnvCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + metadata := expandMetadata(d.Get("metadata").([]interface{})) + d.SetId(buildIdWithVersionKind(metadata, + d.Get("api_version").(string), + d.Get("kind").(string))) + diag := resourceKubernetesEnvUpdate(ctx, d, m) + if diag.HasError() { + d.SetId("") + } + return diag +} + +func resourceKubernetesEnvRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + conn, err := m.(KubeClientsets).DynamicClient() + if err != nil { + return diag.FromErr(err) + } + + gvk, name, namespace, err := util.ParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + // figure out which resource client to use + dc, err := m.(KubeClientsets).DiscoveryClient() + if err != nil { + return diag.FromErr(err) + } + agr, err := restmapper.GetAPIGroupResources(dc) + if err != nil { + return diag.FromErr(err) + } + restMapper := restmapper.NewDiscoveryRESTMapper(agr) + mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return diag.FromErr(err) + } + + // determine if the resource is namespaced or not + var r dynamic.ResourceInterface + if mapping.Scope.Name() == meta.RESTScopeNameNamespace { + if namespace == "" { + namespace = "default" + } + r = conn.Resource(mapping.Resource).Namespace(namespace) + } else { + r = conn.Resource(mapping.Resource) + } + + // get the resource environments + res, err := r.Get(ctx, name, v1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return diag.Diagnostics{{ + Severity: diag.Warning, + Summary: "Resource deleted", + Detail: fmt.Sprintf("The underlying resource %q has been deleted. You should recreate the underlying resource, or remove it from your configuration.", name), + }} + } + return diag.FromErr(err) + } + + // store names of environment variables into map + configuredEnvs := make(map[string]interface{}) + envList := d.Get("env").([]interface{}) + for _, e := range envList { + configuredEnvs[e.(map[string]interface{})["name"].(string)] = "" + } + + // strip out envs not managed by Terraform + managedEnvs, err := getManagedEnvs(res.GetManagedFields(), defaultFieldManagerName, d) + if err != nil { + return diag.FromErr(err) + } + responseEnvs, err := getResponseEnvs(res, d.Get("container").(string)) + if err != nil { + return diag.FromErr(err) + } + + env := []interface{}{} + for _, e := range responseEnvs { + envName := e.(map[string]interface{})["name"].(string) + _, managed := managedEnvs[fmt.Sprintf(`k:{"name":%q}`, envName)] + _, configured := configuredEnvs[envName] + if !managed && !configured { + continue + } + env = append(env, e) + } + + env = flattenEnv(env) + d.Set("env", env) + return nil +} + +func getResponseEnvs(u *unstructured.Unstructured, containerName string) ([]interface{}, error) { + containers, _, _ := unstructured.NestedSlice(u.Object, "spec", "template", "spec", "containers") + for _, c := range containers { + container := c.(map[string]interface{}) + if container["name"].(string) == containerName { + return container["env"].([]interface{}), nil + } + } + return nil, fmt.Errorf("could not find container with name %q", containerName) +} + +// getManagedEnvs reads the field manager metadata to discover which environment variables we're managing +func getManagedEnvs(managedFields []v1.ManagedFieldsEntry, manager string, d *schema.ResourceData) (map[string]interface{}, error) { + var envs map[string]interface{} + for _, m := range managedFields { + if m.Manager != manager { + continue + } + var mm map[string]interface{} + err := json.Unmarshal(m.FieldsV1.Raw, &mm) + if err != nil { + return nil, err + } + spec := mm["f:spec"].(map[string]interface{}) + template := spec["f:template"].(map[string]interface{}) + templateSpec := template["f:spec"].(map[string]interface{}) + containers := templateSpec["f:containers"].(map[string]interface{}) + containerName := fmt.Sprintf(`k:{"name":%q}`, d.Get("container").(string)) + k := containers[containerName].(map[string]interface{}) + if e, ok := k["f:env"].(map[string]interface{}); ok { + envs = e + } + } + return envs, nil +} + +func resourceKubernetesEnvUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + conn, err := m.(KubeClientsets).DynamicClient() + if err != nil { + return diag.FromErr(err) + } + + apiVersion := d.Get("api_version").(string) + kind := d.Get("kind").(string) + metadata := expandMetadata(d.Get("metadata").([]interface{})) + name := metadata.GetName() + namespace := metadata.GetNamespace() + + // figure out which resource client to use + dc, err := m.(KubeClientsets).DiscoveryClient() + if err != nil { + return diag.FromErr(err) + } + agr, err := restmapper.GetAPIGroupResources(dc) + if err != nil { + return diag.FromErr(err) + } + restMapper := restmapper.NewDiscoveryRESTMapper(agr) + gv, err := k8sschema.ParseGroupVersion(apiVersion) + if err != nil { + return diag.FromErr(err) + } + mapping, err := restMapper.RESTMapping(gv.WithKind(kind).GroupKind(), gv.Version) + if err != nil { + return diag.FromErr(err) + } + + // determine if the resource is namespaced or not + var r dynamic.ResourceInterface + namespacedResource := mapping.Scope.Name() == meta.RESTScopeNameNamespace + if namespacedResource { + if namespace == "" { + namespace = "default" + } + r = conn.Resource(mapping.Resource).Namespace(namespace) + } else { + r = conn.Resource(mapping.Resource) + } + + // check the resource exists before we try and patch it + _, err = r.Get(ctx, name, v1.GetOptions{}) + if err != nil { + if d.Id() == "" { + // if we are deleting then there is nothing to do + // if the resource is gone + return nil + } + return diag.Errorf("The resource %q does not exist", name) + } + + patchmeta := map[string]interface{}{ + "name": name, + } + if namespacedResource { + patchmeta["namespace"] = namespace + } + + env := d.Get("env") + env = expandEnv(env.([]interface{})) + if d.Id() == "" { + env = []map[string]interface{}{} + } + + patchobj := map[string]interface{}{ + "apiVersion": apiVersion, + "kind": kind, + "metadata": patchmeta, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": d.Get("container").(string), + "env": env, + }, + }, + }, + }, + }, + } + + patch := unstructured.Unstructured{} + patch.Object = patchobj + patchbytes, err := patch.MarshalJSON() + if err != nil { + return diag.FromErr(err) + } + _, err = r.Patch(ctx, + name, + types.ApplyPatchType, + patchbytes, + v1.PatchOptions{ + FieldManager: d.Get("field_manager").(string), + Force: ptrToBool(d.Get("force").(bool)), + }, + ) + if err != nil { + if errors.IsConflict(err) { + return diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Field manager conflict", + Detail: fmt.Sprintf(`Another client is managing a field Terraform tried to update. Set "force" to true to override: %v`, err), + }} + } + return diag.FromErr(err) + } + + if d.Id() == "" { + // don't try to read if we're deleting + return nil + } + + return resourceKubernetesEnvRead(ctx, d, m) +} + +func resourceKubernetesEnvDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + d.SetId("") + return resourceKubernetesEnvUpdate(ctx, d, m) +} diff --git a/kubernetes/resource_kubernetes_env_test.go b/kubernetes/resource_kubernetes_env_test.go new file mode 100644 index 0000000000..c73766d95e --- /dev/null +++ b/kubernetes/resource_kubernetes_env_test.go @@ -0,0 +1,274 @@ +package kubernetes + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAccKubernetesEnv_basic(t *testing.T) { + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + namespace := "default" + secretName := acctest.RandomWithPrefix("tf-acc-test") + configMapName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "kubernetes_env.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + createEnv(t, name, namespace) + }, + IDRefreshName: resourceName, + ProviderFactories: testAccProviderFactories, + CheckDestroy: func(s *terraform.State) error { + err := confirmExistingEnvs(name, namespace) + if err != nil { + return err + } + return destroyEnv(name, namespace) + }, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesEnv_basic(secretName, configMapName, name, namespace), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "api_version", "apps/v1"), + resource.TestCheckResourceAttr(resourceName, "kind", "Deployment"), + resource.TestCheckResourceAttr(resourceName, "metadata.0.name", name), + resource.TestCheckResourceAttr(resourceName, "env.0.name", "NGINX_HOST"), + resource.TestCheckResourceAttr(resourceName, "env.0.value", "foobar.com"), + resource.TestCheckResourceAttr(resourceName, "env.1.name", "NGINX_PORT"), + resource.TestCheckResourceAttr(resourceName, "env.1.value", "90"), + resource.TestCheckResourceAttr(resourceName, "env.2.value_from.0.secret_key_ref.0.name", secretName), + resource.TestCheckResourceAttr(resourceName, "env.2.value_from.0.secret_key_ref.0.key", "one"), + resource.TestCheckResourceAttr(resourceName, "env.3.value_from.0.config_map_key_ref.0.name", configMapName), + resource.TestCheckResourceAttr(resourceName, "env.3.value_from.0.config_map_key_ref.0.key", "one"), + resource.TestCheckResourceAttr(resourceName, "env.#", "4"), + ), + }, + { + Config: testAccKubernetesEnv_modified(secretName, configMapName, name, namespace), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "api_version", "apps/v1"), + resource.TestCheckResourceAttr(resourceName, "kind", "Deployment"), + resource.TestCheckResourceAttr(resourceName, "metadata.0.name", name), + resource.TestCheckResourceAttr(resourceName, "env.0.name", "NGINX_HOST"), + resource.TestCheckResourceAttr(resourceName, "env.0.value", "hashicorp.com"), + resource.TestCheckResourceAttr(resourceName, "env.1.value_from.0.secret_key_ref.0.name", secretName), + resource.TestCheckResourceAttr(resourceName, "env.1.value_from.0.secret_key_ref.0.key", "two"), + resource.TestCheckResourceAttr(resourceName, "env.2.value_from.0.config_map_key_ref.0.name", configMapName), + resource.TestCheckResourceAttr(resourceName, "env.2.value_from.0.config_map_key_ref.0.key", "three"), + resource.TestCheckResourceAttr(resourceName, "env.#", "3"), + ), + }, + }, + }) +} + +func createEnv(t *testing.T, name, namespace string) error { + conn, err := testAccProvider.Meta().(kubeClientsets).MainClientset() + if err != nil { + return err + } + ctx := context.Background() + var deploy appsv1.Deployment = appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "terraform", + }, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "terraform", + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "nginx", + Image: "nginx", + Env: []v1.EnvVar{ + { + Name: "TEST", + Value: "123", + }, + }, + }, + }, + }, + }, + }, + } + _, err = conn.AppsV1().Deployments(namespace).Create(ctx, &deploy, metav1.CreateOptions{}) + if err != nil { + t.Error("could not create test deployment") + t.Fatal(err) + } + + return err +} + +func destroyEnv(name, namespace string) error { + conn, err := testAccProvider.Meta().(KubeClientsets).MainClientset() + if err != nil { + return err + } + ctx := context.Background() + err = conn.AppsV1().Deployments(namespace).Delete(ctx, name, metav1.DeleteOptions{}) + return err +} + +func confirmExistingEnvs(name, namespace string) error { + conn, err := testAccProvider.Meta().(KubeClientsets).MainClientset() + if err != nil { + return err + } + ctx := context.Background() + deploy, err := conn.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return err + } + env := deploy.Spec.Template.Spec.Containers[0].Env + if len(env) == 0 { + return fmt.Errorf("environment variables not managed by terraform were removed") + } + return err +} + +func testAccKubernetesEnv_basic(secretName, configMapName, name, namespace string) string { + return fmt.Sprintf(`resource "kubernetes_secret" "test" { + metadata { + name = "%s" + } + + data = { + one = "first" + } +} + +resource "kubernetes_config_map" "test" { + metadata { + name = "%s" + } + + data = { + one = "ONE" + } +} + +resource "kubernetes_env" "test" { + container = "nginx" + api_version = "apps/v1" + kind = "Deployment" + metadata { + name = %q + namespace = %q + } + env { + name = "NGINX_HOST" + value = "foobar.com" + } + + env { + name = "NGINX_PORT" + value = "90" + } + + env { + name = "EXPORTED_VARIABLE_FROM_SECRET" + value_from { + secret_key_ref { + name = "${kubernetes_secret.test.metadata.0.name}" + key = "one" + optional = true + } + } + } + + env { + name = "EXPORTED_VARIABLE_FROM_CONFIG_MAP" + value_from { + config_map_key_ref { + name = "${kubernetes_config_map.test.metadata.0.name}" + key = "one" + optional = true + } + } + } + +} + `, secretName, configMapName, name, namespace) +} + +func testAccKubernetesEnv_modified(secretName, configMapName, name, namespace string) string { + return fmt.Sprintf(`resource "kubernetes_secret" "test" { + metadata { + name = "%s" + } + + data = { + one = "first" + } +} + +resource "kubernetes_config_map" "test" { + metadata { + name = "%s" + } + + data = { + one = "ONE" + } +} + +resource "kubernetes_env" "test" { + container = "nginx" + api_version = "apps/v1" + kind = "Deployment" + metadata { + name = %q + namespace = %q + } + env { + name = "NGINX_HOST" + value = "hashicorp.com" + } + + env { + name = "EXPORTED_VARIABLE_FROM_SECRET" + + value_from { + secret_key_ref { + name = "${kubernetes_secret.test.metadata.0.name}" + key = "two" + optional = true + } + } + } + + + env { + name = "EXPORTED_VARIABLE_FROM_CONFIG_MAP" + value_from { + config_map_key_ref { + name = "${kubernetes_config_map.test.metadata.0.name}" + key = "three" + optional = true + } + } + } +} + `, secretName, configMapName, name, namespace) +} diff --git a/kubernetes/structures_env.go b/kubernetes/structures_env.go new file mode 100644 index 0000000000..7ae76c7318 --- /dev/null +++ b/kubernetes/structures_env.go @@ -0,0 +1,190 @@ +package kubernetes + +func expandEnv(e []interface{}) []map[string]interface{} { + envs := []map[string]interface{}{} + if len(e) == 0 { + return envs + } + + for _, c := range e { + p, ok := c.(map[string]interface{}) + if !ok { + continue + } + + newEnv := make(map[string]interface{}) + if name, ok := p["name"]; ok { + newEnv["name"] = name + } + if value, ok := p["value"]; ok { + newEnv["value"] = value + } + if v, ok := p["value_from"].([]interface{}); ok && len(v) > 0 { + var err error + newEnv["valueFrom"], err = expandEnvValueFromMap(v[0]) + if err != nil { + return envs + } + } + envs = append(envs, newEnv) + } + + return envs +} + +func expandEnvValueFromMap(e interface{}) (map[string]interface{}, error) { + if e == nil { + return nil, nil + } + + in := e.(map[string]interface{}) + expandedValues := make(map[string]interface{}) + + if v, ok := in["config_map_key_ref"].([]interface{}); ok && len(v) > 0 { + expandedValues["configMapKeyRef"] = v[0] + } + if v, ok := in["field_ref"].([]interface{}); ok && len(v) > 0 { + expandedValues["fieldRef"] = expandFieldRefMap(v[0]) + } + if v, ok := in["resource_field_ref"].([]interface{}); ok && len(v) > 0 { + expandedValues["resourceFieldRef"] = expandResourceFieldMap(v[0]) + } + if v, ok := in["secret_key_ref"].([]interface{}); ok && len(v) > 0 { + expandedValues["secretKeyRef"] = v[0] + } + + return expandedValues, nil +} + +func expandFieldRefMap(e interface{}) map[string]interface{} { + if e == nil { + return nil + } + + in := e.(map[string]interface{}) + expandedValues := make(map[string]interface{}) + + if v, ok := in["api_version"].(interface{}); ok && v != nil { + expandedValues["apiVersion"] = v + } + if v, ok := in["field_path"].([]interface{}); ok && v != nil { + expandedValues["fieldPath"] = v + } + + return expandedValues +} + +func expandResourceFieldMap(e interface{}) map[string]interface{} { + if e == nil { + return nil + } + + in := e.(map[string]interface{}) + expandedValues := make(map[string]interface{}) + + if v, ok := in["container_name"].(interface{}); ok && v != nil { + expandedValues["containerName"] = v + } + if v, ok := in["divisor"].([]interface{}); ok && v != nil { + expandedValues["divisor"] = v + } + if v, ok := in["resource"].([]interface{}); ok && v != nil { + expandedValues["resource"] = v + } + + return expandedValues +} + +func flattenEnv(e []interface{}) []interface{} { + envs := []interface{}{} + if len(e) == 0 { + return envs + } + for _, c := range e { + p, ok := c.(map[string]interface{}) + if !ok { + continue + } + + newEnv := make(map[string]interface{}) + if name, ok := p["name"]; ok { + newEnv["name"] = name + } + if value, ok := p["value"]; ok && value != "" { + newEnv["value"] = value + } + if v, ok := p["valueFrom"].(map[string]interface{}); ok && len(v) > 0 { + var err error + newEnv["value_from"], err = flattenEnvValueFromMap(v) + if err != nil { + return envs + } + } + envs = append(envs, newEnv) + } + + return envs +} + +func flattenEnvValueFromMap(e interface{}) ([]interface{}, error) { + if e == nil { + return nil, nil + } + + in := e.(map[string]interface{}) + expandedValues := make(map[string]interface{}) + + if v, ok := in["configMapKeyRef"].(interface{}); ok && v != nil { + expandedValues["config_map_key_ref"] = []interface{}{v} + } + if v, ok := in["fieldRef"].(interface{}); ok && v != nil { + expandedValues["field_ref"] = flattenFieldRefMap(v) + } + if v, ok := in["resourceFieldRef"].(interface{}); ok && v != nil { + expandedValues["resource_field_ref"] = flattenResourceFieldMap(v) + } + if v, ok := in["secretKeyRef"].(interface{}); ok && v != nil { + expandedValues["secret_key_ref"] = []interface{}{v} + } + + return []interface{}{expandedValues}, nil +} + +func flattenFieldRefMap(e interface{}) []interface{} { + if e == nil { + return nil + } + + in := e.(map[string]interface{}) + expandedValues := make(map[string]interface{}) + + if v, ok := in["apiVersion"].(interface{}); ok && v != nil { + expandedValues["api_version"] = v + } + if v, ok := in["fieldPath"].(interface{}); ok && v != nil { + expandedValues["field_path"] = v + } + + return []interface{}{expandedValues} +} + +func flattenResourceFieldMap(e interface{}) []interface{} { + if e == nil { + return nil + } + + in := e.(map[string]interface{}) + expandedValues := make(map[string]interface{}) + + if v, ok := in["containerName"].(interface{}); ok && v != nil { + expandedValues["container_name"] = v + } + if v, ok := in["divisor"].(interface{}); ok && v != nil { + expandedValues["divisor"] = v + } + if v, ok := in["resource"].(interface{}); ok && v != nil { + expandedValues["resource"] = v + } + + return []interface{}{expandedValues} +} diff --git a/website/docs/r/env.html.markdown b/website/docs/r/env.html.markdown new file mode 100644 index 0000000000..8e9690b8b0 --- /dev/null +++ b/website/docs/r/env.html.markdown @@ -0,0 +1,109 @@ +--- +subcategory: "core/v1" +layout: "kubernetes" +page_title: "Kubernetes: kubernetes_env" +description: |- + This resource provides a way to manage environment variables in resources that were created outside of Terraform. +--- + +# kubernetes_env + +This resource provides a way to manage environment variables in resources that were created outside of Terraform. This resource provides functionality similar to the `kubectl set env` command. + +## Example Usage + +```hcl +resource "kubernetes_env" "example" { + container = "nginx" + metadata { + name = "nginx-deployment" + } + + api_version = "apps/v1" + kind = "Deployment" + + env { + name = "NGINX_HOST" + value = "google.com" + } + + env { + name = "NGINX_PORT" + value = "90" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `api_version` - (Required) The apiVersion of the resource to add environment variables to. +* `kind` - (Required) The kind of the resource to add environment variables to. +* `metadata` - (Required) Standard metadata of the resource to add environment variables to. +* `container` - (Required) Name of the container for which we are updating the environment variables. +* `env` - (Required) Value block with custom values used to represent environment variables +* `force` - (Optional) Force management of environment variables if there is a conflict. +* `field_manager` - (Optional) The name of the [field manager](https://kubernetes.io/docs/reference/using-api/server-side-apply/#field-management). Defaults to `Terraform`. + +## Nested Blocks + +### `metadata` + +#### Arguments + +* `name` - (Required) Name of the resource to add environment variables to. +* `namespace` - (Optional) Namespace of the resource to add environment variables to. + +### `env` + +#### Arguments + +* `name` - (Required) Name of the environment variable. Must be a C_IDENTIFIER +* `value` - (Optional) Variable references $(VAR_NAME) are expanded using the previous defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. The $(VAR_NAME) syntax can be escaped with a double $$, ie: $$(VAR_NAME). Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "". +* `value_from` - (Optional) Source for the environment variable's value + +### `value_from` + +#### Arguments + +* `config_map_key_ref` - (Optional) Selects a key of a ConfigMap. +* `field_ref` - (Optional) Selects a field of the pod: supports metadata.name, metadata.namespace, metadata.labels, metadata.annotations, spec.nodeName, spec.serviceAccountName, status.podIP. +* `resource_field_ref` - (Optional) Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. +* `secret_key_ref` - (Optional) Selects a key of a secret in the pod's namespace. + +### `config_map_key_ref` + +#### Arguments + +* `key` - (Optional) The key to select. +* `name` - (Optional) Name of the referent. For more info see [Kubernetes reference](http://kubernetes.io/docs/user-guide/identifiers#names) +* `optional` - (Optional) Specify whether the Secret or its key must be defined + +### `field_ref` + +#### Arguments + +* `api_version` - (Optional) Version of the schema the FieldPath is written in terms of, defaults to "v1". +* `field_path` - (Optional) Path of the field to select in the specified API version + +### `resource_field_ref` + +#### Arguments + +* `container_name` - (Optional) The name of the container +* `resource` - (Required) Resource to select +* `divisor` - (Optional) Specifies the output format of the exposed resources, defaults to "1". + +### `secret_key_ref` + +#### Arguments + +* `key` - (Optional) The key of the secret to select from. Must be a valid secret key. +* `name` - (Optional) Name of the referent. For more info see [Kubernetes reference](http://kubernetes.io/docs/user-guide/identifiers#names) +* `optional` - (Optional) Specify whether the Secret or its key must be defined + + +## Import + +This resource does not support the `import` command. As this resource operates on Kubernetes resources that already exist, creating the resource is equivalent to importing it.