Skip to content

Commit af55e47

Browse files
Adding secretV1Data resource (#2604)
* Adding secretV1Data resource * Refactored to use 'any' instead of 'interface{}' in secret_v1_data
1 parent 69263f8 commit af55e47

File tree

5 files changed

+553
-0
lines changed

5 files changed

+553
-0
lines changed

.changelog/2604.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
Adding the `kubernetes_secret_v1_data` resource to the kubernetes provider. This resource will allow users to manage kubernetes secrets
3+
```

docs/resources/secret_v1_data.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
subcategory: "core/v1"
3+
page_title: "Kubernetes: kubernetes_secret_v1_data"
4+
description: |-
5+
This resource allows Terraform to manage the data for a Secret that already exists.
6+
---
7+
8+
# kubernetes_secret_v1_data
9+
10+
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.
11+
12+
<!-- schema generated by tfplugindocs -->
13+
## Schema
14+
15+
### Required
16+
17+
- `data` (Map of String) The data we want to add to the Secret.
18+
- `metadata` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--metadata))
19+
20+
### Optional
21+
22+
- `field_manager` (String) Set the name of the field manager for the specified labels.
23+
- `force` (Boolean) Force overwriting data that is managed outside of Terraform.
24+
25+
### Read-Only
26+
27+
- `id` (String) The ID of this resource.
28+
29+
<a id="nestedblock--metadata"></a>
30+
### Nested Schema for `metadata`
31+
32+
Required:
33+
34+
- `name` (String) The name of the Secret.
35+
36+
Optional:
37+
38+
- `namespace` (String) The namespace of the Secret.
39+
40+
## Example Usage
41+
42+
```terraform
43+
resource "kubernetes_secret_v1_data" "example" {
44+
metadata {
45+
name = "my-secret"
46+
}
47+
data = {
48+
"username" = "admin"
49+
"password" = "s3cr3t"
50+
}
51+
}
52+
```
53+
54+
## Import
55+
56+
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.
57+
58+

kubernetes/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ func Provider() *schema.Provider {
264264
"kubernetes_config_map_v1_data": resourceKubernetesConfigMapV1Data(),
265265
"kubernetes_secret": resourceKubernetesSecretV1(),
266266
"kubernetes_secret_v1": resourceKubernetesSecretV1(),
267+
"kubernetes_secret_v1_data": resourceKubernetesSecretV1Data(),
267268
"kubernetes_pod": resourceKubernetesPodV1(),
268269
"kubernetes_pod_v1": resourceKubernetesPodV1(),
269270
"kubernetes_endpoints": resourceKubernetesEndpointsV1(),
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package kubernetes
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"fmt"
10+
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13+
14+
"k8s.io/apimachinery/pkg/api/errors"
15+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17+
"k8s.io/apimachinery/pkg/types"
18+
"k8s.io/utils/ptr"
19+
)
20+
21+
func resourceKubernetesSecretV1Data() *schema.Resource {
22+
return &schema.Resource{
23+
CreateContext: resourceKubernetesSecretV1DataCreate,
24+
ReadContext: resourceKubernetesSecretV1DataRead,
25+
UpdateContext: resourceKubernetesSecretV1DataUpdate,
26+
DeleteContext: resourceKubernetesSecretV1DataDelete,
27+
28+
Schema: map[string]*schema.Schema{
29+
"metadata": {
30+
Type: schema.TypeList,
31+
Description: "Metadata for the kubernetes Secret.",
32+
Required: true,
33+
MaxItems: 1,
34+
Elem: &schema.Resource{
35+
Schema: map[string]*schema.Schema{
36+
"name": {
37+
Type: schema.TypeString,
38+
Description: "The name of the Secret.",
39+
Required: true,
40+
ForceNew: true,
41+
},
42+
"namespace": {
43+
Type: schema.TypeString,
44+
Description: "The namespace of the Secret.",
45+
Optional: true,
46+
ForceNew: true,
47+
Default: "default",
48+
},
49+
},
50+
},
51+
},
52+
"data": {
53+
Type: schema.TypeMap,
54+
Description: "Data to be stored in the Kubernetes Secret.",
55+
Required: true,
56+
Elem: &schema.Schema{
57+
Type: schema.TypeString,
58+
},
59+
},
60+
"force": {
61+
Type: schema.TypeBool,
62+
Description: "Flag to force updates to the Kubernetes Secret.",
63+
Optional: true,
64+
},
65+
"field_manager": {
66+
Type: schema.TypeString,
67+
Description: "Set the name of the field manager for the specified labels",
68+
Optional: true,
69+
Default: defaultFieldManagerName,
70+
},
71+
},
72+
}
73+
}
74+
75+
func resourceKubernetesSecretV1DataCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
76+
metadata := expandMetadata(d.Get("metadata").([]any))
77+
// Sets the resource id based on the metadata
78+
d.SetId(buildId(metadata))
79+
80+
//Calling the update function ensuring resource config is correct
81+
diag := resourceKubernetesSecretV1DataUpdate(ctx, d, m)
82+
if diag.HasError() {
83+
d.SetId("")
84+
}
85+
return diag
86+
}
87+
88+
// Retrieves the current state of the k8s secret, and update the current sate
89+
func resourceKubernetesSecretV1DataRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
90+
conn, err := m.(KubeClientsets).MainClientset()
91+
if err != nil {
92+
return diag.FromErr(err)
93+
}
94+
95+
namespace, name, err := idParts(d.Id())
96+
if err != nil {
97+
return diag.FromErr(err)
98+
}
99+
100+
// getting the secret data
101+
res, err := conn.CoreV1().Secrets(namespace).Get(ctx, name, v1.GetOptions{})
102+
if err != nil {
103+
if errors.IsNotFound(err) {
104+
return diag.Diagnostics{{
105+
Severity: diag.Warning,
106+
Summary: "Secret deleted",
107+
Detail: fmt.Sprintf("The underlying secret %q has been deleted. You should recreate the underlying secret, or remove it from your configuration.", name),
108+
}}
109+
}
110+
return diag.FromErr(err)
111+
}
112+
113+
configuredData := d.Get("data").(map[string]any)
114+
115+
// stripping out the data not managed by Terraform
116+
fieldManagerName := d.Get("field_manager").(string)
117+
118+
managedSecretData, err := getManagedSecretData(res.GetManagedFields(), fieldManagerName)
119+
if err != nil {
120+
return diag.FromErr(err)
121+
}
122+
data := res.Data
123+
for k := range data {
124+
_, managed := managedSecretData["f:"+k]
125+
_, configured := configuredData[k]
126+
if !managed && !configured {
127+
delete(data, k)
128+
}
129+
130+
}
131+
decodedData := make(map[string]string, len(data))
132+
for k, v := range data {
133+
decodedData[k] = string(v)
134+
}
135+
136+
d.Set("data", decodedData)
137+
138+
return nil
139+
}
140+
141+
// getManagedSecretData reads the field manager metadata to discover which fields we're managing
142+
func getManagedSecretData(managedFields []v1.ManagedFieldsEntry, manager string) (map[string]interface{}, error) {
143+
var data map[string]any
144+
for _, m := range managedFields {
145+
// Only consider entries managed by the specified manager
146+
if m.Manager != manager {
147+
continue
148+
}
149+
var mm map[string]any
150+
err := json.Unmarshal(m.FieldsV1.Raw, &mm)
151+
if err != nil {
152+
return nil, err
153+
}
154+
// Check if the "data" field exists and extract it
155+
if l, ok := mm["f:data"].(map[string]any); ok {
156+
data = l
157+
}
158+
}
159+
return data, nil
160+
}
161+
162+
func resourceKubernetesSecretV1DataUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
163+
conn, err := m.(KubeClientsets).MainClientset()
164+
if err != nil {
165+
return diag.FromErr(err)
166+
}
167+
168+
metadata := expandMetadata(d.Get("metadata").([]any))
169+
name := metadata.GetName()
170+
namespace := metadata.GetNamespace()
171+
172+
_, err = conn.CoreV1().Secrets(namespace).Get(ctx, name, v1.GetOptions{})
173+
if err != nil {
174+
if d.Id() == "" {
175+
// If we are deleting then there is nothing to do if the resource is gone
176+
return nil
177+
}
178+
if statusErr, ok := err.(*errors.StatusError); ok && errors.IsNotFound(statusErr) {
179+
return diag.Errorf("The Secret %q does not exist", name)
180+
}
181+
return diag.Errorf("Have got the following error while validating the existence of the Secret %q: %v", name, err)
182+
}
183+
184+
// Craft the patch to update the data
185+
data := d.Get("data").(map[string]any)
186+
if d.Id() == "" {
187+
// If we're deleting then we just patch with an empty data map
188+
data = map[string]interface{}{}
189+
}
190+
191+
encodedData := make(map[string][]byte, len(data))
192+
for k, v := range data {
193+
encodedData[k] = []byte(v.(string))
194+
}
195+
196+
patchobj := map[string]any{
197+
"apiVersion": "v1",
198+
"kind": "Secret",
199+
"metadata": map[string]any{
200+
"name": name,
201+
"namespace": namespace,
202+
},
203+
"data": encodedData,
204+
}
205+
patch := unstructured.Unstructured{}
206+
patch.Object = patchobj
207+
patchbytes, err := patch.MarshalJSON()
208+
if err != nil {
209+
return diag.FromErr(err)
210+
}
211+
212+
// Apply the patch
213+
_, err = conn.CoreV1().Secrets(namespace).Patch(ctx,
214+
name,
215+
types.ApplyPatchType,
216+
patchbytes,
217+
v1.PatchOptions{
218+
FieldManager: d.Get("field_manager").(string),
219+
Force: ptr.To(d.Get("force").(bool)),
220+
},
221+
)
222+
if err != nil {
223+
if errors.IsConflict(err) {
224+
return diag.Diagnostics{{
225+
Severity: diag.Error,
226+
Summary: "Field manager conflict",
227+
Detail: fmt.Sprintf("Another client is managing a field Terraform tried to update. Set 'force' to true to override: %v", err),
228+
}}
229+
}
230+
return diag.FromErr(err)
231+
}
232+
233+
if d.Id() == "" {
234+
return nil
235+
}
236+
237+
return resourceKubernetesSecretV1DataRead(ctx, d, m)
238+
}
239+
240+
func resourceKubernetesSecretV1DataDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
241+
// sets resource id to an empty. Simulating the deletion.
242+
d.SetId("")
243+
// Now we are calling the update function, to update the resource state
244+
return resourceKubernetesSecretV1DataUpdate(ctx, d, m)
245+
}

0 commit comments

Comments
 (0)