From 9570a036ac9f0e9a5b457b4836b399d8f299d19e Mon Sep 17 00:00:00 2001 From: Jaylon McShan Date: Thu, 23 May 2024 14:53:34 -0500 Subject: [PATCH 1/9] Adding initial configuration for the new resource --- ...resource_kubernetes_secret_v1_data_test.go | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 kubernetes/resource_kubernetes_secret_v1_data_test.go diff --git a/kubernetes/resource_kubernetes_secret_v1_data_test.go b/kubernetes/resource_kubernetes_secret_v1_data_test.go new file mode 100644 index 0000000000..36a3deda0b --- /dev/null +++ b/kubernetes/resource_kubernetes_secret_v1_data_test.go @@ -0,0 +1,123 @@ +package kubernetes + +import ( + "context" + "encoding/base64" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func resourceKubernetesSecretV1Data() *schema.Resource { + return &schema.Resource{ + Create: resourceKubernetesSecretV1DataCreate, + Read: resourceKubernetesSecretV1DataRead, + Update: resourceKubernetesSecretV1DataUpdate, + Delete: resourceKubernetesSecretV1DataDelete, + + Schema: map[string]*schema.Schema{ + // meta attr, which contains info about the secret. It is required and can have a maxvalue of 1 + "metadata": { + Type: schema.TypeList, + Description: "Metadata for the kubernetes Secret.", + Required: true, + MaxItems: 1, + Elem: resourceMetaData(), + }, + // map data attr, contains data to be store in secret. Elem, specifies the schema for each value in the map + "data": { + Type: schema.TypeMap, + Description: "Data to be stored in the Kubernetes Secret.", + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "force": { + Type: schema.TypeBool, + Description: "Flag to force updates to the Kubernetes Secret.", + Optional: true, + Default: false, + }, + }, + } +} + +func resourceKubernetesSecretV1DataCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + metadata := expandMetaData(d.Get("metadata").([]interface{})) + // Sets the resource id based on the metadata + d.SetId(buildId(metadata)) + + //Calling the update function ensuring resource config is correct + diag := resourceKubernetesSecretV1DataUpdate(ctx, d, m) + if diag.HasError() { + d.SetId("") + } + return diag +} + +// Retrieves rthe current state of the k8s secret, and update the current sate +func resourceKubernetesSecretV1DataRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + clientset, err := getClientset(m) + if err != nil { + return diag.FromErr(err) + } + + //extracting ns and name from res id + namespace, name, err := parseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + // Retrieve the K8s secret + secret, err := clientset.Corev1.Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + // Handle case where the Secret is not found + return diag.Diagnostics{{ + Severity: diag.Warning, + Summary: "Secret deleted", + Detail: fmt.Sprintf("The underlying secret %q has been deleted. You should recreate the underlying secret, or remove it from your configuration.", name), + }} + } + return diag.FromErr(err) + } + + // Extract managed data from the secret + managedData, err := getManagedSecretData(secret) + if err != nil { + return diag.FromErr(err) + } + + // filter out the data not managed by terraform + configuredData := d.Get("data").(map[string]interface{}) + for k := range managedData { + if _, exists := configuredData[k]; !exists { + delete(managedData, k) + } + } + // Update the state with the managed data + d.Set("data", managedData) +} + +// extracts data from the secret that is managed by terraform +func getManagedSecretData(secret *v1.Secret) (map[string]interface{}, error) { + managedData := make(map[string]interface{}) + + //looping through all data in the secret + for key, value := range secret.Data { + // decode base64-encoded value + decodedValue, err := base64.StdEncoding.DecodeString(string(value)) + if err != nil { + return nil, fmt.Errorf("failed to decode value for key %q: %w", key, err) + } + + // just storing the decoded value I got in the managed data map + managedData[key] = string(decodedValue) + } + return managedData, nil + +} From 4d1ced63f3f3251dbf2fe2a3a4469a5028735b65 Mon Sep 17 00:00:00 2001 From: Jaylon McShan Date: Mon, 3 Jun 2024 15:27:17 -0500 Subject: [PATCH 2/9] Added field_manager attribute to the schema --- kubernetes/resource_kubernetes_secret_v1.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/kubernetes/resource_kubernetes_secret_v1.go b/kubernetes/resource_kubernetes_secret_v1.go index df9c8a90c9..ef69286aa6 100644 --- a/kubernetes/resource_kubernetes_secret_v1.go +++ b/kubernetes/resource_kubernetes_secret_v1.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -87,6 +88,13 @@ func resourceKubernetesSecretV1() *schema.Resource { Default: true, Description: "Terraform will wait for the service account token to be created.", }, + "field_manager": { + Type: schema.TypeString, + Description: "Set the name of the field manager for the specified labels", + Optional: true, + Default: defaultFieldManagerName, + ValidateFunc: validation.StringIsNotWhiteSpace, + }, }, Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(1 * time.Minute), From 2c6b9c625fb2f98ce22543560f65a1140f332542 Mon Sep 17 00:00:00 2001 From: Jaylon McShan Date: Wed, 5 Jun 2024 10:33:04 -0500 Subject: [PATCH 3/9] Adding field_manager to the schema, and removing the unncessary validator package in secret_v1.go --- kubernetes/resource_kubernetes_secret_v1.go | 8 -------- kubernetes/resource_kubernetes_secret_v1_data.go | 8 ++++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/kubernetes/resource_kubernetes_secret_v1.go b/kubernetes/resource_kubernetes_secret_v1.go index ef69286aa6..df9c8a90c9 100644 --- a/kubernetes/resource_kubernetes_secret_v1.go +++ b/kubernetes/resource_kubernetes_secret_v1.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -88,13 +87,6 @@ func resourceKubernetesSecretV1() *schema.Resource { Default: true, Description: "Terraform will wait for the service account token to be created.", }, - "field_manager": { - Type: schema.TypeString, - Description: "Set the name of the field manager for the specified labels", - Optional: true, - Default: defaultFieldManagerName, - ValidateFunc: validation.StringIsNotWhiteSpace, - }, }, Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(1 * time.Minute), diff --git a/kubernetes/resource_kubernetes_secret_v1_data.go b/kubernetes/resource_kubernetes_secret_v1_data.go index b3b2d4e57c..597977d017 100644 --- a/kubernetes/resource_kubernetes_secret_v1_data.go +++ b/kubernetes/resource_kubernetes_secret_v1_data.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -62,6 +63,13 @@ func resourceKubernetesSecretV1Data() *schema.Resource { Optional: true, Default: false, }, + "field_manager": { + Type: schema.TypeString, + Description: "Set the name of the field manager for the specified labels", + Optional: true, + Default: defaultFieldManagerName, + ValidateFunc: validation.StringIsNotWhiteSpace, + }, }, } } From d4eead4f594f60d542bd36000eb8360f5f82acf8 Mon Sep 17 00:00:00 2001 From: Jaylon McShan Date: Mon, 14 Oct 2024 11:09:49 -0500 Subject: [PATCH 4/9] Current state of resource --- .../resource_kubernetes_secret_v1_data.go | 118 ++++++++++-------- ...resource_kubernetes_secret_v1_data_test.go | 44 ++++--- 2 files changed, 97 insertions(+), 65 deletions(-) diff --git a/kubernetes/resource_kubernetes_secret_v1_data.go b/kubernetes/resource_kubernetes_secret_v1_data.go index 597977d017..f63b6b62f6 100644 --- a/kubernetes/resource_kubernetes_secret_v1_data.go +++ b/kubernetes/resource_kubernetes_secret_v1_data.go @@ -2,15 +2,14 @@ package kubernetes import ( "context" - "encoding/base64" + "encoding/json" "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" @@ -24,7 +23,6 @@ func resourceKubernetesSecretV1Data() *schema.Resource { DeleteContext: resourceKubernetesSecretV1DataDelete, Schema: map[string]*schema.Schema{ - // meta attr, which contains info about the secret. It is required and can have a maxvalue of 1 "metadata": { Type: schema.TypeList, Description: "Metadata for the kubernetes Secret.", @@ -34,13 +32,13 @@ func resourceKubernetesSecretV1Data() *schema.Resource { Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, - Description: "The name of the ConfigMap.", + Description: "The name of the Secret.", Required: true, ForceNew: true, }, "namespace": { Type: schema.TypeString, - Description: "The namespace of the ConfigMap.", + Description: "The namespace of the Secret.", Optional: true, ForceNew: true, Default: "default", @@ -48,7 +46,6 @@ func resourceKubernetesSecretV1Data() *schema.Resource { }, }, }, - // map data attr, contains data to be store in secret. Elem, specifies the schema for each value in the map "data": { Type: schema.TypeMap, Description: "Data to be stored in the Kubernetes Secret.", @@ -61,14 +58,14 @@ func resourceKubernetesSecretV1Data() *schema.Resource { Type: schema.TypeBool, Description: "Flag to force updates to the Kubernetes Secret.", Optional: true, - Default: false, + //Default: true, }, "field_manager": { - Type: schema.TypeString, - Description: "Set the name of the field manager for the specified labels", - Optional: true, - Default: defaultFieldManagerName, - ValidateFunc: validation.StringIsNotWhiteSpace, + Type: schema.TypeString, + Description: "Set the name of the field manager for the specified labels", + Optional: true, + Default: defaultFieldManagerName, + //ValidateFunc: validation.StringIsNotWhiteSpace, }, }, } @@ -84,7 +81,7 @@ func resourceKubernetesSecretV1DataCreate(ctx context.Context, d *schema.Resourc if diag.HasError() { d.SetId("") } - return nil + return diag } // Retrieves the current state of the k8s secret, and update the current sate @@ -94,16 +91,15 @@ func resourceKubernetesSecretV1DataRead(ctx context.Context, d *schema.ResourceD return diag.FromErr(err) } - //extracting ns and name from res id namespace, name, err := idParts(d.Id()) if err != nil { return diag.FromErr(err) } - // Retrieve the K8s secret - secret, err := conn.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) + + // getting the secret data + res, err := conn.CoreV1().Secrets(namespace).Get(ctx, name, v1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { - // Handle case where the Secret is not found return diag.Diagnostics{{ Severity: diag.Warning, Summary: "Secret deleted", @@ -113,41 +109,52 @@ func resourceKubernetesSecretV1DataRead(ctx context.Context, d *schema.ResourceD return diag.FromErr(err) } - // Extract managed data from the secret - managedData, err := getManagedSecretData(secret) + configuredData := d.Get("data").(map[string]interface{}) + + // stripping out the data not managed by Terraform + fieldManagerName := d.Get("field_manager").(string) + + managedSecretData, err := getManagedSecretData(res.GetManagedFields(), fieldManagerName) if err != nil { return diag.FromErr(err) } - - // filter out the data not managed by terraform - configuredData := d.Get("data").(map[string]interface{}) - for k := range managedData { - if _, exists := configuredData[k]; !exists { - delete(managedData, k) + data := res.Data + for k := range data { + _, managed := managedSecretData["f:"+k] + _, configured := configuredData[k] + if !managed && !configured { + delete(data, k) } } - // Update the state with the managed data - d.Set("data", managedData) + decodedData := make(map[string]string, len(data)) + for k, v := range data { + decodedData[k] = string(v) + } + + d.Set("data", decodedData) + return nil } -// extracts data from the secret that is managed by terraform -func getManagedSecretData(secret *v1.Secret) (map[string]interface{}, error) { - managedData := make(map[string]interface{}) - - //looping through all data in the secret - for key, value := range secret.Data { - // decode base64-encoded value - decodedValue, err := base64.StdEncoding.DecodeString(string(value)) +// getManagedSecretData reads the field manager metadata to discover which fields we're managing +func getManagedSecretData(managedFields []v1.ManagedFieldsEntry, manager string) (map[string]interface{}, error) { + var data map[string]interface{} + for _, m := range managedFields { + // Only consider entries managed by the specified manager + if m.Manager != manager { + continue + } + var mm map[string]interface{} + err := json.Unmarshal(m.FieldsV1.Raw, &mm) if err != nil { - return nil, fmt.Errorf("failed to decode value for key %q: %w", key, err) + return nil, err + } + // Check if the "data" field exists and extract it + if l, ok := mm["f:data"].(map[string]interface{}); ok { + data = l } - - // just storing the decoded value I got in the managed data map - managedData[key] = string(decodedValue) } - return managedData, nil - + return data, nil } func resourceKubernetesSecretV1DataUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { @@ -160,7 +167,7 @@ func resourceKubernetesSecretV1DataUpdate(ctx context.Context, d *schema.Resourc name := metadata.GetName() namespace := metadata.GetNamespace() - _, err = conn.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) + _, err = conn.CoreV1().Secrets(namespace).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 @@ -171,12 +178,23 @@ func resourceKubernetesSecretV1DataUpdate(ctx context.Context, d *schema.Resourc } return diag.Errorf("Have got the following error while validating the existence of the Secret %q: %v", name, err) } + // Craft the patch to update the data - data := d.Get("data") + dataInterface := d.Get("data") + data, ok := dataInterface.(map[string]interface{}) + if !ok { + return diag.Errorf("Error casting data to map[string]interface{}") + } if d.Id() == "" { // If we're deleting then we just patch with an empty data map data = map[string]interface{}{} } + + encodedData := make(map[string][]byte, len(data)) + for k, v := range data { + encodedData[k] = []byte(v.(string)) + } + patchobj := map[string]interface{}{ "apiVersion": "v1", "kind": "Secret", @@ -184,7 +202,7 @@ func resourceKubernetesSecretV1DataUpdate(ctx context.Context, d *schema.Resourc "name": name, "namespace": namespace, }, - "data": data, + "data": encodedData, } patch := unstructured.Unstructured{} patch.Object = patchobj @@ -192,12 +210,13 @@ func resourceKubernetesSecretV1DataUpdate(ctx context.Context, d *schema.Resourc if err != nil { return diag.FromErr(err) } + // Apply the patch _, err = conn.CoreV1().Secrets(namespace).Patch(ctx, name, types.ApplyPatchType, patchbytes, - metav1.PatchOptions{ + v1.PatchOptions{ FieldManager: d.Get("field_manager").(string), Force: ptr.To(d.Get("force").(bool)), }, @@ -207,16 +226,17 @@ func resourceKubernetesSecretV1DataUpdate(ctx context.Context, d *schema.Resourc 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), + 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() == "" { return nil } - return resourceKubernetesSecretV1DataRead(ctx, d, m) + return resourceKubernetesSecretV1DataRead(ctx, d, m) } func resourceKubernetesSecretV1DataDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { diff --git a/kubernetes/resource_kubernetes_secret_v1_data_test.go b/kubernetes/resource_kubernetes_secret_v1_data_test.go index 1dbbe67ea0..d38e8dace7 100644 --- a/kubernetes/resource_kubernetes_secret_v1_data_test.go +++ b/kubernetes/resource_kubernetes_secret_v1_data_test.go @@ -21,15 +21,11 @@ func TestAccKubernetesSecretV1Data_basic(t *testing.T) { // Creating unique names to ensure tests are isolated name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) - data := map[string][]byte{ - "key1": []byte("value1"), - "key2": []byte("value2"), - } // Running the test case resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) - createSecret(name, namespace, data) + createSecret(name, namespace) }, IDRefreshName: resourceName, IDRefreshIgnore: []string{"metadata.0.resource_version"}, @@ -39,49 +35,62 @@ func TestAccKubernetesSecretV1Data_basic(t *testing.T) { }, Steps: []resource.TestStep{ { - // Test case for a empty secret + // Test case for an empty secret Config: testAccKubernetesSecretV1Data_empty(name), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "metadata.0.name", name), resource.TestCheckResourceAttr(resourceName, "data.%", "0"), + resource.TestCheckResourceAttr(resourceName, "field_manager", "tftest"), ), }, { - // test case for a secret with some data + // Test case for a secret with some data Config: testAccKubernetesSecretV1Data_basic(name), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "metadata.0.name", name), resource.TestCheckResourceAttr(resourceName, "data.%", "2"), resource.TestCheckResourceAttr(resourceName, "data.key1", "value1"), resource.TestCheckResourceAttr(resourceName, "data.key2", "value2"), + resource.TestCheckResourceAttr(resourceName, "field_manager", "tftest"), ), }, { - // testing a modified secret + // Testing a modified secret Config: testAccKubernetesSecretV1Data_modified(name), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "metadata.0.name", name), resource.TestCheckResourceAttr(resourceName, "data.%", "2"), - resource.TestCheckResourceAttr(resourceName, "data.key1", "new_value1"), - resource.TestCheckResourceAttr(resourceName, "data.key3", "value3"), + resource.TestCheckResourceAttr(resourceName, "data.key1", "one"), + resource.TestCheckResourceAttr(resourceName, "data.key3", "three"), + resource.TestCheckResourceAttr(resourceName, "field_manager", "tftest"), ), }, { // Testing a secret that doesn't exist - Config: testAccKubernetesSecretV1Data_empty(name), - ExpectError: regexp.MustCompile("The secret .* does not exist"), + Config: testAccKubernetesSecretV1Data_empty(name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "metadata.0.name", name), + resource.TestCheckResourceAttr(resourceName, "data.%", "0"), + resource.TestCheckResourceAttr(resourceName, "field_manager", "tftest"), + ), }, }, }) } // Create a kubernetes secret -func createSecret(name, namespace string, data map[string][]byte) error { +func createSecret(name, namespace string) error { conn, err := testAccProvider.Meta().(KubeClientsets).MainClientset() if err != nil { return err } ctx := context.Background() + + data := map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + } + secret := v1.Secret{} secret.SetName(name) secret.SetNamespace(namespace) @@ -128,6 +137,8 @@ func testAccKubernetesSecretV1Data_empty(name string) string { name = %q } data = {} + field_manager = "tftest" + } `, name) } @@ -143,11 +154,11 @@ resource "kubernetes_secret_v1_data" "test" { "key1" = "value1" "key2" = "value2" } + field_manager = "tftest" } `, name) } -// Generating some basic config, for a modified secret func testAccKubernetesSecretV1Data_modified(name string) string { return fmt.Sprintf(` resource "kubernetes_secret_v1_data" "test" { @@ -155,9 +166,10 @@ resource "kubernetes_secret_v1_data" "test" { name = %q } data = { - "key1" = "new_value1" - "key3" = "value3" + "key1" = "one" + "key3" = "three" } + field_manager = "tftest" } `, name) } From f55ea0dcb1ac2ae877d54185336c60280c571eeb Mon Sep 17 00:00:00 2001 From: Jaylon McShan Date: Tue, 15 Oct 2024 12:34:42 -0500 Subject: [PATCH 5/9] Adding changelog entry for the new resource --- .changelog/2505.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/2505.txt diff --git a/.changelog/2505.txt b/.changelog/2505.txt new file mode 100644 index 0000000000..bfb657a013 --- /dev/null +++ b/.changelog/2505.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +Adding the `kubernetes_secret_v1_data` resource to the kubernetes provider. This resource will allow users to manage kubernetes secrets +``` \ No newline at end of file From 15baf34ff60c73c6f648fc7edc3410b63056b4c8 Mon Sep 17 00:00:00 2001 From: Jaylon McShan Date: Tue, 15 Oct 2024 12:49:04 -0500 Subject: [PATCH 6/9] Adding documentation for the secret_v1_data resource --- docs/resources/secret_v1_data.md | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 docs/resources/secret_v1_data.md diff --git a/docs/resources/secret_v1_data.md b/docs/resources/secret_v1_data.md new file mode 100644 index 0000000000..4f8d92f68f --- /dev/null +++ b/docs/resources/secret_v1_data.md @@ -0,0 +1,58 @@ +--- +subcategory: "core/v1" +page_title: "Kubernetes: kubernetes_secret_v1_data" +description: |- + This resource allows Terraform to manage the data for a Secret that already exists. +--- + +# kubernetes_secret_v1_data + +This resource allows Terraform to manage data within a pre-existing Secret. This resource uses [field management](https://kubernetes.io/docs/reference/using-api/server-side-apply/#field-management) and [server-side apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) to manage only the data that is defined in the Terraform configuration. Existing data not specified in the configuration will be ignored. If data specified in the config is already managed by another client, it will cause a conflict which can be overridden by setting `force` to true. + + +## Schema + +### Required + +- `data` (Map of String) The data we want to add to the Secret. +- `metadata` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--metadata)) + +### Optional + +- `field_manager` (String) Set the name of the field manager for the specified labels. +- `force` (Boolean) Force overwriting data that is managed outside of Terraform. + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `metadata` + +Required: + +- `name` (String) The name of the Secret. + +Optional: + +- `namespace` (String) The namespace of the Secret. + +## Example Usage + +```terraform +resource "kubernetes_secret_v1_data" "example" { + metadata { + name = "my-secret" + } + data = { + "username" = "admin" + "password" = "s3cr3t" + } +} +``` + +## 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. + + From 8d44021c0fbd69955ae43ea0bf08a4e03e505be3 Mon Sep 17 00:00:00 2001 From: Jaylon McShan Date: Tue, 15 Oct 2024 12:53:42 -0500 Subject: [PATCH 7/9] Adding license headers for new resource / test file --- kubernetes/resource_kubernetes_secret_v1_data.go | 3 +++ kubernetes/resource_kubernetes_secret_v1_data_test.go | 3 +++ kubernetes/resource_kubernetes_secret_v1_test.go | 1 - 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/kubernetes/resource_kubernetes_secret_v1_data.go b/kubernetes/resource_kubernetes_secret_v1_data.go index f63b6b62f6..df43a712ad 100644 --- a/kubernetes/resource_kubernetes_secret_v1_data.go +++ b/kubernetes/resource_kubernetes_secret_v1_data.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package kubernetes import ( diff --git a/kubernetes/resource_kubernetes_secret_v1_data_test.go b/kubernetes/resource_kubernetes_secret_v1_data_test.go index d38e8dace7..b84ac9b7ad 100644 --- a/kubernetes/resource_kubernetes_secret_v1_data_test.go +++ b/kubernetes/resource_kubernetes_secret_v1_data_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package kubernetes import ( diff --git a/kubernetes/resource_kubernetes_secret_v1_test.go b/kubernetes/resource_kubernetes_secret_v1_test.go index fa10861b46..8f881f16b9 100644 --- a/kubernetes/resource_kubernetes_secret_v1_test.go +++ b/kubernetes/resource_kubernetes_secret_v1_test.go @@ -1,6 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 - package kubernetes import ( From cda8cba3691a8c8c73a7b4e6e27991bc27bae2f4 Mon Sep 17 00:00:00 2001 From: Jaylon McShan Date: Thu, 17 Oct 2024 09:02:22 -0500 Subject: [PATCH 8/9] Updated test handling field manager conflict error --- ...resource_kubernetes_secret_v1_data_test.go | 122 ++++++++++++++---- 1 file changed, 95 insertions(+), 27 deletions(-) diff --git a/kubernetes/resource_kubernetes_secret_v1_data_test.go b/kubernetes/resource_kubernetes_secret_v1_data_test.go index b84ac9b7ad..a5c50afc63 100644 --- a/kubernetes/resource_kubernetes_secret_v1_data_test.go +++ b/kubernetes/resource_kubernetes_secret_v1_data_test.go @@ -1,6 +1,5 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 - package kubernetes import ( @@ -16,19 +15,16 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// This test function tests the basic func of the secret resource "secret_v1" -func TestAccKubernetesSecretV1Data_basic(t *testing.T) { - // Setting up the test parameters - resourceName := "kubernetes_secret_v1_data.test" - namespace := "default" - // Creating unique names to ensure tests are isolated +// Handling the case for a empty secret +func TestAccKubernetesSecretV1Data_empty(t *testing.T) { name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + namespace := "default" + resourceName := "kubernetes_secret_v1_data.test" - // Running the test case resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) - createSecret(name, namespace) + createEmptySecret(name, namespace) }, IDRefreshName: resourceName, IDRefreshIgnore: []string{"metadata.0.resource_version"}, @@ -38,7 +34,6 @@ func TestAccKubernetesSecretV1Data_basic(t *testing.T) { }, Steps: []resource.TestStep{ { - // Test case for an empty secret Config: testAccKubernetesSecretV1Data_empty(name), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "metadata.0.name", name), @@ -46,8 +41,46 @@ func TestAccKubernetesSecretV1Data_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "field_manager", "tftest"), ), }, + }, + }) +} + +func createEmptySecret(name, namespace string) error { + conn, err := testAccProvider.Meta().(KubeClientsets).MainClientset() + if err != nil { + return err + } + ctx := context.Background() + + secret := v1.Secret{} + secret.SetName(name) + secret.SetNamespace(namespace) + secret.Data = map[string][]byte{} + _, err = conn.CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{ + FieldManager: "tftest", + }) + return err +} + +// Handling the case of secret creation with basic data +func TestAccKubernetesSecretV1Data_basic_data(t *testing.T) { + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + namespace := "default" + resourceName := "kubernetes_secret_v1_data.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + createSecretWithData(name, namespace) + }, + IDRefreshName: resourceName, + IDRefreshIgnore: []string{"metadata.0.resource_version"}, + ProviderFactories: testAccProviderFactories, + CheckDestroy: func(s *terraform.State) error { + return destroySecret(name, namespace) + }, + Steps: []resource.TestStep{ { - // Test case for a secret with some data Config: testAccKubernetesSecretV1Data_basic(name), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "metadata.0.name", name), @@ -57,8 +90,51 @@ func TestAccKubernetesSecretV1Data_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "field_manager", "tftest"), ), }, + }, + }) +} + +func createSecretWithData(name, namespace string) error { + conn, err := testAccProvider.Meta().(KubeClientsets).MainClientset() + if err != nil { + return err + } + ctx := context.Background() + + data := map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + } + + secret := v1.Secret{} + secret.SetName(name) + secret.SetNamespace(namespace) + secret.Data = data + _, err = conn.CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{ + FieldManager: "tftest", + }) + return err +} + +// Handling the case for a modified secret +func TestAccKubernetesSecretV1Data_modified(t *testing.T) { + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + namespace := "default" + resourceName := "kubernetes_secret_v1_data.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + createModifiedSecret(name, namespace) + }, + IDRefreshName: resourceName, + IDRefreshIgnore: []string{"metadata.0.resource_version"}, + ProviderFactories: testAccProviderFactories, + CheckDestroy: func(s *terraform.State) error { + return destroySecret(name, namespace) + }, + Steps: []resource.TestStep{ { - // Testing a modified secret Config: testAccKubernetesSecretV1Data_modified(name), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "metadata.0.name", name), @@ -68,21 +144,11 @@ func TestAccKubernetesSecretV1Data_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "field_manager", "tftest"), ), }, - { - // Testing a secret that doesn't exist - Config: testAccKubernetesSecretV1Data_empty(name), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "metadata.0.name", name), - resource.TestCheckResourceAttr(resourceName, "data.%", "0"), - resource.TestCheckResourceAttr(resourceName, "field_manager", "tftest"), - ), - }, }, }) } -// Create a kubernetes secret -func createSecret(name, namespace string) error { +func createModifiedSecret(name, namespace string) error { conn, err := testAccProvider.Meta().(KubeClientsets).MainClientset() if err != nil { return err @@ -90,15 +156,17 @@ func createSecret(name, namespace string) error { ctx := context.Background() data := map[string][]byte{ - "key1": []byte("value1"), - "key2": []byte("value2"), + "key1": []byte("one"), + "key3": []byte("three"), } secret := v1.Secret{} secret.SetName(name) secret.SetNamespace(namespace) secret.Data = data - _, err = conn.CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{}) + _, err = conn.CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{ + FieldManager: "tftest", + }) return err } @@ -146,7 +214,7 @@ func testAccKubernetesSecretV1Data_empty(name string) string { `, name) } -// Generate some basic config, with a secret with test data +// Generate some basic config, with a secret with basic data func testAccKubernetesSecretV1Data_basic(name string) string { return fmt.Sprintf(` resource "kubernetes_secret_v1_data" "test" { From 3269a9ef23fde1a6cb66af2689b1a0118fb803b7 Mon Sep 17 00:00:00 2001 From: BBBmau Date: Thu, 17 Oct 2024 12:11:28 -0700 Subject: [PATCH 9/9] cleanup --- kubernetes/resource_kubernetes_secret_v1_data.go | 3 +-- kubernetes/resource_kubernetes_secret_v1_test.go | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kubernetes/resource_kubernetes_secret_v1_data.go b/kubernetes/resource_kubernetes_secret_v1_data.go index df43a712ad..082aaaf0f8 100644 --- a/kubernetes/resource_kubernetes_secret_v1_data.go +++ b/kubernetes/resource_kubernetes_secret_v1_data.go @@ -61,14 +61,12 @@ func resourceKubernetesSecretV1Data() *schema.Resource { Type: schema.TypeBool, Description: "Flag to force updates to the Kubernetes Secret.", Optional: true, - //Default: true, }, "field_manager": { Type: schema.TypeString, Description: "Set the name of the field manager for the specified labels", Optional: true, Default: defaultFieldManagerName, - //ValidateFunc: validation.StringIsNotWhiteSpace, }, }, } @@ -128,6 +126,7 @@ func resourceKubernetesSecretV1DataRead(ctx context.Context, d *schema.ResourceD if !managed && !configured { delete(data, k) } + } decodedData := make(map[string]string, len(data)) for k, v := range data { diff --git a/kubernetes/resource_kubernetes_secret_v1_test.go b/kubernetes/resource_kubernetes_secret_v1_test.go index 8f881f16b9..fa10861b46 100644 --- a/kubernetes/resource_kubernetes_secret_v1_test.go +++ b/kubernetes/resource_kubernetes_secret_v1_test.go @@ -1,5 +1,6 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 + package kubernetes import (