Skip to content

Commit 6d5814e

Browse files
committed
feat(jobs): support secret manager references in serverless jobs definitions
1 parent 6f2f988 commit 6d5814e

16 files changed

+8924
-297
lines changed

cmd/tftemplate/main.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import (
66
"log"
77
"text/template"
88

9-
"tftemplate/models"
10-
119
"github.com/AlecAivazis/survey/v2"
10+
"tftemplate/models"
1211
)
1312

1413
var (

internal/services/jobs/definition.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package jobs
22

33
import (
44
"context"
5+
"regexp"
56
"time"
67

78
"github.com/hashicorp/go-cty/cty"
@@ -90,6 +91,43 @@ func ResourceDefinition() *schema.Resource {
9091
},
9192
"region": regional.Schema(),
9293
"project_id": account.ProjectIDSchema(),
94+
"secret_reference": {
95+
Type: schema.TypeSet,
96+
Optional: true,
97+
Description: "A reference to a Secret Manager secret.",
98+
Elem: &schema.Resource{
99+
Schema: map[string]*schema.Schema{
100+
"secret_id": {
101+
Type: schema.TypeString,
102+
Description: "The secret UUID.",
103+
Required: true,
104+
ValidateFunc: validation.IsUUID,
105+
},
106+
"secret_reference_id": {
107+
Type: schema.TypeString,
108+
Computed: true,
109+
Description: "The secret reference UUID",
110+
},
111+
"secret_version": {
112+
Type: schema.TypeString,
113+
Description: "The secret version, default to Latest.",
114+
Required: true,
115+
},
116+
"file": {
117+
Type: schema.TypeString,
118+
Optional: true,
119+
Description: "The absolute file path where the secret will be mounted.",
120+
ValidateFunc: validation.StringMatch(regexp.MustCompile(`^(/[^/]+)+$`), "must be an absolute path to the file"),
121+
},
122+
"environment": {
123+
Type: schema.TypeString,
124+
Optional: true,
125+
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"),
127+
},
128+
},
129+
},
130+
},
93131
},
94132
}
95133
}
@@ -132,6 +170,12 @@ func ResourceJobDefinitionCreate(ctx context.Context, d *schema.ResourceData, m
132170
return diag.FromErr(err)
133171
}
134172

173+
if rawSecretReference, ok := d.GetOk("secret_reference"); ok {
174+
if err := CreateJobDefinitionSecret(rawSecretReference, api, region, definition.ID); err != nil {
175+
return diag.FromErr(err)
176+
}
177+
}
178+
135179
d.SetId(regional.NewIDString(region, definition.ID))
136180

137181
return ResourceJobDefinitionRead(ctx, d, m)
@@ -157,6 +201,33 @@ func ResourceJobDefinitionRead(ctx context.Context, d *schema.ResourceData, m in
157201
return diag.FromErr(err)
158202
}
159203

204+
rawSecretRefs, err := api.ListJobDefinitionSecrets(&jobs.ListJobDefinitionSecretsRequest{
205+
Region: region,
206+
JobDefinitionID: id,
207+
})
208+
if err != nil {
209+
return diag.FromErr(err)
210+
}
211+
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+
160231
_ = d.Set("name", definition.Name)
161232
_ = d.Set("cpu_limit", int(definition.CPULimit))
162233
_ = d.Set("memory_limit", int(definition.MemoryLimit))
@@ -168,6 +239,7 @@ func ResourceJobDefinitionRead(ctx context.Context, d *schema.ResourceData, m in
168239
_ = d.Set("cron", flattenJobDefinitionCron(definition.CronSchedule))
169240
_ = d.Set("region", definition.Region)
170241
_ = d.Set("project_id", definition.ProjectID)
242+
_ = d.Set("secret_reference", secretRefs)
171243

172244
return nil
173245
}
@@ -231,6 +303,27 @@ func ResourceJobDefinitionUpdate(ctx context.Context, d *schema.ResourceData, m
231303
req.CronSchedule = expandJobDefinitionCron(d.Get("cron")).ToUpdateRequest()
232304
}
233305

306+
if d.HasChange("secret_reference") {
307+
oldRawSecretRefs, _ := d.GetChange("secret_reference")
308+
oldSecretRefs := expandJobDefinitionSecret(oldRawSecretRefs)
309+
310+
for _, oldSecretRef := range oldSecretRefs {
311+
if err := api.DeleteJobDefinitionSecret(&jobs.DeleteJobDefinitionSecretRequest{
312+
Region: region,
313+
JobDefinitionID: id,
314+
SecretID: oldSecretRef.SecretReferenceID,
315+
}); err != nil {
316+
return diag.FromErr(err)
317+
}
318+
}
319+
320+
if rawSecretReference, ok := d.GetOk("secret_reference"); ok {
321+
if err := CreateJobDefinitionSecret(rawSecretReference, api, region, id); err != nil {
322+
return diag.FromErr(err)
323+
}
324+
}
325+
}
326+
234327
if _, err := api.UpdateJobDefinition(req, scw.WithContext(ctx)); err != nil {
235328
return diag.FromErr(err)
236329
}

internal/services/jobs/definition_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package jobs_test
22

33
import (
44
"fmt"
5+
"regexp"
56
"testing"
67

78
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
@@ -160,6 +161,169 @@ func TestAccJobDefinition_Cron(t *testing.T) {
160161
})
161162
}
162163

164+
func TestAccJobDefinition_SecretReference(t *testing.T) {
165+
tt := acctest.NewTestTools(t)
166+
defer tt.Cleanup()
167+
resource.ParallelTest(t, resource.TestCase{
168+
PreCheck: func() { acctest.PreCheck(t) },
169+
ProviderFactories: tt.ProviderFactories,
170+
CheckDestroy: testAccCheckJobDefinitionDestroy(tt),
171+
Steps: []resource.TestStep{
172+
{
173+
Config: `
174+
resource "scaleway_secret" "main" {
175+
name = "job-secret"
176+
path = "/one"
177+
}
178+
resource "scaleway_secret_version" "main" {
179+
secret_id = scaleway_secret.main.id
180+
data = "your_secret"
181+
}
182+
locals {
183+
parts = split("/", scaleway_secret.main.id)
184+
secret_uuid = local.parts[1]
185+
}
186+
187+
resource scaleway_job_definition main {
188+
name = "test-jobs-job-definition-secret"
189+
cpu_limit = 120
190+
memory_limit = 256
191+
image_uri = "docker.io/alpine:latest"
192+
secret_reference {
193+
secret_id = local.secret_uuid
194+
secret_version = "latest"
195+
file = "/home/dev/env"
196+
}
197+
secret_reference {
198+
secret_id = local.secret_uuid
199+
secret_version = "latest"
200+
environment = "SOME_ENV"
201+
}
202+
}
203+
`,
204+
Check: resource.ComposeTestCheckFunc(
205+
testAccCheckJobDefinitionExists(tt, "scaleway_job_definition.main"),
206+
acctest.CheckResourceAttrUUID("scaleway_job_definition.main", "id"),
207+
resource.TestCheckResourceAttr("scaleway_job_definition.main", "name", "test-jobs-job-definition-secret"),
208+
resource.TestCheckResourceAttr("scaleway_job_definition.main", "secret_reference.#", "2"),
209+
resource.TestCheckResourceAttr("scaleway_job_definition.main", "secret_reference.0.file", "/home/dev/env"),
210+
resource.TestCheckResourceAttr("scaleway_job_definition.main", "secret_reference.1.environment", "SOME_ENV"),
211+
),
212+
},
213+
{
214+
Config: `
215+
resource "scaleway_secret" "main" {
216+
name = "job-secret"
217+
path = "/one"
218+
}
219+
resource "scaleway_secret_version" "main" {
220+
secret_id = scaleway_secret.main.id
221+
data = "your_secret"
222+
}
223+
locals {
224+
parts = split("/", scaleway_secret.main.id)
225+
secret_uuid = local.parts[1]
226+
}
227+
228+
resource scaleway_job_definition main {
229+
name = "test-jobs-job-definition-secret"
230+
cpu_limit = 120
231+
memory_limit = 256
232+
image_uri = "docker.io/alpine:latest"
233+
secret_reference {
234+
secret_id = local.secret_uuid
235+
secret_version = "latest"
236+
file = "/home/dev/new_env"
237+
}
238+
secret_reference {
239+
secret_id = local.secret_uuid
240+
secret_version = "latest"
241+
environment = "SOME_ENV"
242+
}
243+
}
244+
`,
245+
Check: resource.ComposeTestCheckFunc(
246+
testAccCheckJobDefinitionExists(tt, "scaleway_job_definition.main"),
247+
acctest.CheckResourceAttrUUID("scaleway_job_definition.main", "id"),
248+
resource.TestCheckResourceAttr("scaleway_job_definition.main", "name", "test-jobs-job-definition-secret"),
249+
resource.TestCheckResourceAttr("scaleway_job_definition.main", "secret_reference.#", "2"),
250+
resource.TestCheckResourceAttr("scaleway_job_definition.main", "secret_reference.0.file", "/home/dev/new_env"),
251+
resource.TestCheckResourceAttr("scaleway_job_definition.main", "secret_reference.1.environment", "SOME_ENV"),
252+
),
253+
},
254+
},
255+
})
256+
}
257+
258+
func TestAccJobDefinition_WrongSecretReference(t *testing.T) {
259+
tt := acctest.NewTestTools(t)
260+
defer tt.Cleanup()
261+
262+
resource.ParallelTest(t, resource.TestCase{
263+
PreCheck: func() { acctest.PreCheck(t) },
264+
ProviderFactories: tt.ProviderFactories,
265+
CheckDestroy: testAccCheckJobDefinitionDestroy(tt),
266+
Steps: []resource.TestStep{
267+
{
268+
Config: `
269+
resource "scaleway_secret" "main" {
270+
name = "job-secret"
271+
}
272+
resource "scaleway_secret_version" "main" {
273+
secret_id = scaleway_secret.main.id
274+
data = "your_secret"
275+
}
276+
locals {
277+
parts = split("/", scaleway_secret.main.id)
278+
secret_uuid = local.parts[1]
279+
}
280+
281+
resource scaleway_job_definition main {
282+
name = "test-jobs-job-definition-secret"
283+
cpu_limit = 120
284+
memory_limit = 256
285+
image_uri = "docker.io/alpine:latest"
286+
secret_reference {
287+
secret_id = local.secret_uuid
288+
secret_version = "1"
289+
}
290+
}
291+
`,
292+
ExpectError: regexp.MustCompile(`the secret .+ is missing a mount point.+`),
293+
},
294+
{
295+
Config: `
296+
resource "scaleway_secret" "main" {
297+
name = "job-secret"
298+
}
299+
resource "scaleway_secret_version" "main" {
300+
secret_id = scaleway_secret.main.id
301+
data = "your_secret"
302+
}
303+
locals {
304+
parts = split("/", scaleway_secret.main.id)
305+
secret_uuid = local.parts[1]
306+
}
307+
308+
resource scaleway_job_definition main {
309+
name = "test-jobs-job-definition-secret"
310+
cpu_limit = 120
311+
memory_limit = 256
312+
image_uri = "docker.io/alpine:latest"
313+
secret_reference {
314+
secret_id = local.secret_uuid
315+
secret_version = "1"
316+
environment = "SOME_ENV"
317+
file = "/home/dev/env"
318+
}
319+
}
320+
`,
321+
ExpectError: regexp.MustCompile(`the secret .+ must have exactly one mount point.+`),
322+
},
323+
},
324+
})
325+
}
326+
163327
func testAccCheckJobDefinitionExists(tt *acctest.TestTools, n string) resource.TestCheckFunc {
164328
return func(state *terraform.State) error {
165329
rs, ok := state.RootModule().Resources[n]

0 commit comments

Comments
 (0)