Skip to content

Commit a829db7

Browse files
committed
fix(jobs): support secret regional and uuid, fix diff detection and update
1 parent 097302f commit a829db7

File tree

5 files changed

+1372
-398
lines changed

5 files changed

+1372
-398
lines changed

internal/services/jobs/helpers.go

Lines changed: 114 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package jobs
22

33
import (
4+
"bytes"
5+
"context"
46
"fmt"
57

68
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -93,7 +95,7 @@ func flattenJobDefinitionCron(cron *jobs.CronSchedule) []any {
9395

9496
type JobDefinitionSecret struct {
9597
SecretReferenceID string
96-
SecretID string
98+
SecretID regional.ID
9799
SecretVersion string
98100
File string
99101
Environment string
@@ -119,7 +121,7 @@ func expandJobDefinitionSecret(i any) []JobDefinitionSecret {
119121
}
120122

121123
secret := JobDefinitionSecret{
122-
SecretID: secretMap["secret_id"].(string),
124+
SecretID: regional.ExpandID(secretMap["secret_id"].(string)),
123125
SecretVersion: secretMap["secret_version"].(string),
124126
File: file,
125127
Environment: env,
@@ -134,45 +136,132 @@ func expandJobDefinitionSecret(i any) []JobDefinitionSecret {
134136
return parsedSecrets
135137
}
136138

137-
func CreateJobDefinitionSecret(rawSecretReference any, api *jobs.API, region scw.Region, jobID string) error {
138-
parsedSecretReferences := expandJobDefinitionSecret(rawSecretReference)
139-
secrets := []*jobs.CreateJobDefinitionSecretsRequestSecretConfig{}
139+
func flattenJobDefinitionSecret(jobSecrets []*jobs.Secret) []any {
140+
secretRefs := make([]interface{}, len(jobSecrets))
140141

141-
for _, parsedSecretRef := range parsedSecretReferences {
142-
var secretConfig *jobs.CreateJobDefinitionSecretsRequestSecretConfig
142+
for i, secret := range jobSecrets {
143+
secretRef := make(map[string]interface{})
144+
secretRef["secret_id"] = secret.SecretManagerID
145+
secretRef["secret_reference_id"] = secret.SecretID
146+
secretRef["secret_version"] = secret.SecretManagerVersion
143147

144-
if parsedSecretRef.Environment != "" {
145-
secretConfig = &jobs.CreateJobDefinitionSecretsRequestSecretConfig{
146-
SecretManagerID: parsedSecretRef.SecretID,
147-
SecretManagerVersion: parsedSecretRef.SecretVersion,
148-
EnvVarName: &parsedSecretRef.Environment,
149-
}
148+
if secret.File != nil {
149+
secretRef["file"] = secret.File.Path
150150
}
151151

152-
if parsedSecretRef.File != "" {
153-
secretConfig = &jobs.CreateJobDefinitionSecretsRequestSecretConfig{
154-
SecretManagerID: parsedSecretRef.SecretID,
155-
SecretManagerVersion: parsedSecretRef.SecretVersion,
156-
Path: &parsedSecretRef.File,
152+
if secret.EnvVar != nil {
153+
secretRef["environment"] = secret.EnvVar.Name
154+
}
155+
156+
secretRefs[i] = secretRef
157+
}
158+
159+
return secretRefs
160+
}
161+
162+
func CreateJobDefinitionSecret(ctx context.Context, api *jobs.API, jobSecrets []JobDefinitionSecret, region scw.Region, jobID string) error {
163+
secretConfigs := []*jobs.CreateJobDefinitionSecretsRequestSecretConfig{}
164+
165+
for _, parsedSecretRef := range jobSecrets {
166+
secretConfig := &jobs.CreateJobDefinitionSecretsRequestSecretConfig{}
167+
168+
secretConfigs = append(secretConfigs, secretConfig)
169+
170+
if parsedSecretRef.SecretID.Region.String() != "" {
171+
if parsedSecretRef.SecretID.Region.String() != region.String() {
172+
return fmt.Errorf("the secret id %s is in the region %s, expected %s", parsedSecretRef.SecretID, parsedSecretRef.SecretID.Region, region)
157173
}
158174
}
159175

160-
if parsedSecretRef.Environment != "" && parsedSecretRef.File != "" {
161-
return fmt.Errorf("the secret id %s must have exactly one mount point: file or environment", parsedSecretRef.SecretID)
176+
secretConfig.SecretManagerID = parsedSecretRef.SecretID.ID
177+
secretConfig.SecretManagerVersion = parsedSecretRef.SecretVersion
178+
179+
if err := validateJobDefinitionSecret(&parsedSecretRef); err != nil {
180+
return err
162181
}
163182

164-
if parsedSecretRef.Environment == "" && parsedSecretRef.File == "" {
165-
return fmt.Errorf("the secret id %s is missing a mount point: file or environment", parsedSecretRef.SecretID)
183+
if parsedSecretRef.Environment != "" {
184+
secretConfig.EnvVarName = &parsedSecretRef.Environment
166185
}
167186

168-
secrets = append(secrets, secretConfig)
187+
if parsedSecretRef.File != "" {
188+
secretConfig.Path = &parsedSecretRef.File
189+
}
169190
}
170191

171192
_, err := api.CreateJobDefinitionSecrets(&jobs.CreateJobDefinitionSecretsRequest{
172193
Region: region,
173194
JobDefinitionID: jobID,
174-
Secrets: secrets,
175-
})
195+
Secrets: secretConfigs,
196+
}, scw.WithContext(ctx))
176197

177198
return err
178199
}
200+
201+
func hashJobDefinitionSecret(secret *JobDefinitionSecret) int {
202+
buf := bytes.NewBufferString(secret.SecretID.String())
203+
buf.WriteString(secret.SecretVersion)
204+
205+
if secret.Environment != "" {
206+
buf.WriteString(secret.Environment)
207+
}
208+
209+
if secret.File != "" {
210+
buf.WriteString(secret.File)
211+
}
212+
213+
return schema.HashString(buf.String())
214+
}
215+
216+
func DiffJobDefinitionSecrets(oldSecretRefs, newSecretRefs []JobDefinitionSecret) (toCreate []JobDefinitionSecret, toDelete []JobDefinitionSecret, err error) {
217+
toCreate = make([]JobDefinitionSecret, 0)
218+
toDelete = make([]JobDefinitionSecret, 0)
219+
220+
// hash the new and old secret sets
221+
oldSecretRefsMap := make(map[int]JobDefinitionSecret, len(oldSecretRefs))
222+
for _, secret := range oldSecretRefs {
223+
oldSecretRefsMap[hashJobDefinitionSecret(&secret)] = secret
224+
}
225+
226+
newSecretRefsMap := make(map[int]JobDefinitionSecret, len(newSecretRefs))
227+
228+
for _, secret := range newSecretRefs {
229+
if err := validateJobDefinitionSecret(&secret); err != nil {
230+
return toCreate, toDelete, err
231+
}
232+
233+
newSecretRefsMap[hashJobDefinitionSecret(&secret)] = secret
234+
}
235+
236+
// filter secrets to delete
237+
for hash, secret := range oldSecretRefsMap {
238+
if _, ok := newSecretRefsMap[hash]; !ok {
239+
toDelete = append(toDelete, secret)
240+
}
241+
}
242+
243+
// filter secrets to create
244+
for hash, secret := range newSecretRefsMap {
245+
if _, ok := oldSecretRefsMap[hash]; !ok {
246+
toCreate = append(toCreate, secret)
247+
}
248+
}
249+
250+
return toCreate, toDelete, nil
251+
}
252+
253+
func validateJobDefinitionSecret(secret *JobDefinitionSecret) error {
254+
if secret == nil {
255+
return nil
256+
}
257+
258+
if secret.Environment != "" && secret.File != "" {
259+
return fmt.Errorf("the secret id %s must have exactly one mount point: file or environment", secret.SecretID)
260+
}
261+
262+
if secret.Environment == "" && secret.File == "" {
263+
return fmt.Errorf("the secret id %s is missing a mount point: file or environment", secret.SecretID)
264+
}
265+
266+
return nil
267+
}

internal/services/jobs/definition.go renamed to internal/services/jobs/jobs.go

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/scaleway/scaleway-sdk-go/scw"
1414
"github.com/scaleway/terraform-provider-scaleway/v2/internal/dsf"
1515
"github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors"
16+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality"
1617
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/regional"
1718
"github.com/scaleway/terraform-provider-scaleway/v2/internal/services/account"
1819
"github.com/scaleway/terraform-provider-scaleway/v2/internal/types"
@@ -95,13 +96,22 @@ func ResourceDefinition() *schema.Resource {
9596
Type: schema.TypeSet,
9697
Optional: true,
9798
Description: "A reference to a Secret Manager secret.",
99+
Set: func(v interface{}) int {
100+
secret := v.(map[string]interface{})
101+
if secret["environment"] != "" {
102+
return schema.HashString(locality.ExpandID(secret["secret_id"].(string)) + secret["secret_version"].(string) + secret["environment"].(string))
103+
}
104+
105+
return schema.HashString(locality.ExpandID(secret["secret_id"].(string)) + secret["secret_version"].(string) + secret["file"].(string))
106+
},
98107
Elem: &schema.Resource{
99108
Schema: map[string]*schema.Schema{
100109
"secret_id": {
101-
Type: schema.TypeString,
102-
Description: "The secret UUID.",
103-
Required: true,
104-
ValidateFunc: validation.IsUUID,
110+
Type: schema.TypeString,
111+
Description: "The secret unique identifier, it could be formatted as region/UUID or UUID. In case the region is passed, it must be the same as the job definition.",
112+
Required: true,
113+
DiffSuppressOnRefresh: true,
114+
DiffSuppressFunc: dsf.Locality,
105115
},
106116
"secret_reference_id": {
107117
Type: schema.TypeString,
@@ -110,8 +120,9 @@ func ResourceDefinition() *schema.Resource {
110120
},
111121
"secret_version": {
112122
Type: schema.TypeString,
113-
Description: "The secret version, default to Latest.",
114-
Required: true,
123+
Description: "The secret version.",
124+
Default: "latest",
125+
Optional: true,
115126
},
116127
"file": {
117128
Type: schema.TypeString,
@@ -123,7 +134,7 @@ func ResourceDefinition() *schema.Resource {
123134
Type: schema.TypeString,
124135
Optional: true,
125136
Description: "An environment variable containing the secret value.",
126-
ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[A-Z]+(_[A-Z]+)*$`), "environment variable must be composed of uppercase letters separated by an underscore"),
137+
ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[A-Z|0-9]+(_[A-Z|0-9]+)*$`), "environment variable must be composed of uppercase letters separated by an underscore"),
127138
},
128139
},
129140
},
@@ -171,7 +182,7 @@ func ResourceJobDefinitionCreate(ctx context.Context, d *schema.ResourceData, m
171182
}
172183

173184
if rawSecretReference, ok := d.GetOk("secret_reference"); ok {
174-
if err := CreateJobDefinitionSecret(rawSecretReference, api, region, definition.ID); err != nil {
185+
if err := CreateJobDefinitionSecret(ctx, api, expandJobDefinitionSecret(rawSecretReference), region, definition.ID); err != nil {
175186
return diag.FromErr(err)
176187
}
177188
}
@@ -209,25 +220,6 @@ func ResourceJobDefinitionRead(ctx context.Context, d *schema.ResourceData, m in
209220
return diag.FromErr(err)
210221
}
211222

212-
secretRefs := make([]interface{}, len(rawSecretRefs.Secrets))
213-
214-
for i, secret := range rawSecretRefs.Secrets {
215-
secretRef := make(map[string]interface{})
216-
secretRef["secret_id"] = secret.SecretManagerID
217-
secretRef["secret_reference_id"] = secret.SecretID
218-
secretRef["secret_version"] = secret.SecretManagerVersion
219-
220-
if secret.File != nil {
221-
secretRef["file"] = secret.File.Path
222-
}
223-
224-
if secret.EnvVar != nil {
225-
secretRef["environment"] = secret.EnvVar.Name
226-
}
227-
228-
secretRefs[i] = secretRef
229-
}
230-
231223
_ = d.Set("name", definition.Name)
232224
_ = d.Set("cpu_limit", int(definition.CPULimit))
233225
_ = d.Set("memory_limit", int(definition.MemoryLimit))
@@ -239,7 +231,7 @@ func ResourceJobDefinitionRead(ctx context.Context, d *schema.ResourceData, m in
239231
_ = d.Set("cron", flattenJobDefinitionCron(definition.CronSchedule))
240232
_ = d.Set("region", definition.Region)
241233
_ = d.Set("project_id", definition.ProjectID)
242-
_ = d.Set("secret_reference", secretRefs)
234+
_ = d.Set("secret_reference", flattenJobDefinitionSecret(rawSecretRefs.Secrets))
243235

244236
return nil
245237
}
@@ -304,21 +296,29 @@ func ResourceJobDefinitionUpdate(ctx context.Context, d *schema.ResourceData, m
304296
}
305297

306298
if d.HasChange("secret_reference") {
307-
oldRawSecretRefs, _ := d.GetChange("secret_reference")
299+
oldRawSecretRefs, newRawSecretRefs := d.GetChange("secret_reference")
300+
308301
oldSecretRefs := expandJobDefinitionSecret(oldRawSecretRefs)
302+
newSecretRefs := expandJobDefinitionSecret(newRawSecretRefs)
309303

310-
for _, oldSecretRef := range oldSecretRefs {
311-
if err := api.DeleteJobDefinitionSecret(&jobs.DeleteJobDefinitionSecretRequest{
304+
toCreate, toDelete, err := DiffJobDefinitionSecrets(oldSecretRefs, newSecretRefs)
305+
if err != nil {
306+
return diag.FromErr(err)
307+
}
308+
309+
for _, secret := range toDelete {
310+
deleteReq := &jobs.DeleteJobDefinitionSecretRequest{
312311
Region: region,
313312
JobDefinitionID: id,
314-
SecretID: oldSecretRef.SecretReferenceID,
315-
}); err != nil {
313+
SecretID: secret.SecretReferenceID,
314+
}
315+
if err := api.DeleteJobDefinitionSecret(deleteReq, scw.WithContext(ctx)); err != nil {
316316
return diag.FromErr(err)
317317
}
318318
}
319319

320-
if rawSecretReference, ok := d.GetOk("secret_reference"); ok {
321-
if err := CreateJobDefinitionSecret(rawSecretReference, api, region, id); err != nil {
320+
if len(toCreate) > 0 {
321+
if err := CreateJobDefinitionSecret(ctx, api, toCreate, region, id); err != nil {
322322
return diag.FromErr(err)
323323
}
324324
}

0 commit comments

Comments
 (0)