Skip to content

Commit b10d9eb

Browse files
authored
fix(containers): handle secret_environment_variable lifecycle (#3039)
1 parent e361946 commit b10d9eb

File tree

7 files changed

+559
-441
lines changed

7 files changed

+559
-441
lines changed

Diff for: go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/scaleway/terraform-provider-scaleway/v2
33
go 1.24.0
44

55
require (
6+
github.com/alexedwards/argon2id v1.0.0
67
github.com/aws/aws-sdk-go-v2 v1.36.3
78
github.com/aws/aws-sdk-go-v2/config v1.29.9
89
github.com/aws/aws-sdk-go-v2/credentials v1.17.62

Diff for: go.sum

+15
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXx
2020
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
2121
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
2222
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
23+
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
24+
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
2325
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
2426
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
2527
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
@@ -368,6 +370,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
368370
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
369371
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
370372
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
373+
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
371374
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
372375
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
373376
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
@@ -376,6 +379,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
376379
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
377380
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
378381
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
382+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
379383
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
380384
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
381385
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -394,6 +398,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
394398
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
395399
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
396400
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
401+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
397402
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
398403
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
399404
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -410,19 +415,28 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
410415
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
411416
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
412417
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
418+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
413419
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
420+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
421+
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
414422
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
415423
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
416424
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
417425
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
418426
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
427+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
428+
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
429+
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
419430
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
420431
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
421432
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
422433
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
423434
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
424435
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
425436
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
437+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
438+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
439+
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
426440
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
427441
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
428442
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
@@ -434,6 +448,7 @@ golang.org/x/tools v0.0.0-20200214201135-548b770e2dfa/go.mod h1:TB2adYChydJhpapK
434448
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
435449
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
436450
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
451+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
437452
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
438453
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
439454
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

Diff for: internal/dsf/strings.go

+10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dsf
33
import (
44
"strings"
55

6+
"github.com/alexedwards/argon2id"
67
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
78
)
89

@@ -13,3 +14,12 @@ func IgnoreCase(_, oldValue, newValue string, _ *schema.ResourceData) bool {
1314
func IgnoreCaseAndHyphen(_, oldValue, newValue string, _ *schema.ResourceData) bool {
1415
return strings.ReplaceAll(strings.ToLower(oldValue), "-", "_") == strings.ReplaceAll(strings.ToLower(newValue), "-", "_")
1516
}
17+
18+
func CompareArgon2idPasswordAndHash(_, oldValue, newValue string, _ *schema.ResourceData) bool {
19+
match, err := argon2id.ComparePasswordAndHash(newValue, oldValue)
20+
if err != nil {
21+
return false
22+
}
23+
24+
return match
25+
}

Diff for: internal/services/container/container.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ func ResourceContainer() *schema.Resource {
7575
Type: schema.TypeString,
7676
ValidateFunc: validation.StringLenBetween(0, 1000),
7777
},
78-
ValidateDiagFunc: validation.MapKeyLenBetween(0, 100),
78+
ValidateDiagFunc: validation.MapKeyLenBetween(0, 100),
79+
DiffSuppressFunc: dsf.CompareArgon2idPasswordAndHash,
80+
DiffSuppressOnRefresh: true,
7981
},
8082
"min_scale": {
8183
Type: schema.TypeInt,
@@ -358,6 +360,7 @@ func ResourceContainerRead(ctx context.Context, d *schema.ResourceData, m interf
358360
_ = d.Set("scaling_option", flattenScalingOption(co.ScalingOption))
359361
_ = d.Set("region", co.Region.String())
360362
_ = d.Set("local_storage_limit", int(co.LocalStorageLimit))
363+
_ = d.Set("secret_environment_variables", flattenContainerSecrets(co.SecretEnvironmentVariables))
361364

362365
return nil
363366
}
@@ -393,7 +396,8 @@ func ResourceContainerUpdate(ctx context.Context, d *schema.ResourceData, m inte
393396
}
394397

395398
if d.HasChanges("secret_environment_variables") {
396-
req.SecretEnvironmentVariables = expandContainerSecrets(d.Get("secret_environment_variables"))
399+
oldEnv, newEnv := d.GetChange("secret_environment_variables")
400+
req.SecretEnvironmentVariables = FilterSecretEnvsToPatch(expandContainerSecrets(oldEnv), expandContainerSecrets(newEnv))
397401
}
398402

399403
if d.HasChanges("min_scale") {

Diff for: internal/services/container/container_test.go

+53-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package container_test
22

33
import (
4+
"errors"
45
"fmt"
56
"testing"
67

8+
"github.com/alexedwards/argon2id"
79
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
810
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
911
containerSDK "github.com/scaleway/scaleway-sdk-go/api/container/v1beta1"
1012
"github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest"
1113
"github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors"
1214
"github.com/scaleway/terraform-provider-scaleway/v2/internal/services/container"
1315
containerchecks "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/container/testfuncs"
16+
"github.com/stretchr/testify/assert"
1417
)
1518

1619
func TestAccContainer_Basic(t *testing.T) {
@@ -142,6 +145,7 @@ func TestAccContainer_Env(t *testing.T) {
142145
}
143146
secret_environment_variables = {
144147
"test_secret" = "test_secret"
148+
"first_secret" = "first_secret"
145149
}
146150
}
147151
`,
@@ -150,7 +154,8 @@ func TestAccContainer_Env(t *testing.T) {
150154
acctest.CheckResourceAttrUUID("scaleway_container_namespace.main", "id"),
151155
acctest.CheckResourceAttrUUID("scaleway_container.main", "id"),
152156
resource.TestCheckResourceAttr("scaleway_container.main", "environment_variables.test", "test"),
153-
resource.TestCheckResourceAttr("scaleway_container.main", "secret_environment_variables.test_secret", "test_secret"),
157+
passwordMatchHash("scaleway_container.main", "secret_environment_variables.test_secret", "test_secret"),
158+
passwordMatchHash("scaleway_container.main", "secret_environment_variables.first_secret", "first_secret"),
154159
),
155160
},
156161
{
@@ -165,6 +170,7 @@ func TestAccContainer_Env(t *testing.T) {
165170
}
166171
secret_environment_variables = {
167172
"foo_secret" = "bar_secret"
173+
"test_secret" = "updated_secret"
168174
}
169175
}
170176
`,
@@ -173,7 +179,9 @@ func TestAccContainer_Env(t *testing.T) {
173179
acctest.CheckResourceAttrUUID("scaleway_container_namespace.main", "id"),
174180
acctest.CheckResourceAttrUUID("scaleway_container.main", "id"),
175181
resource.TestCheckResourceAttr("scaleway_container.main", "environment_variables.foo", "bar"),
176-
resource.TestCheckResourceAttr("scaleway_container.main", "secret_environment_variables.foo_secret", "bar_secret"),
182+
passwordMatchHash("scaleway_container.main", "secret_environment_variables.foo_secret", "bar_secret"),
183+
passwordMatchHash("scaleway_container.main", "secret_environment_variables.test_secret", "updated_secret"),
184+
resource.TestCheckNoResourceAttr("scaleway_container.main", "secret_environment_variables.first_secret"),
177185
),
178186
},
179187
{
@@ -601,3 +609,46 @@ func isContainerDestroyed(tt *acctest.TestTools) resource.TestCheckFunc {
601609
return nil
602610
}
603611
}
612+
613+
func passwordMatchHash(parent string, key string, password string) resource.TestCheckFunc {
614+
return func(state *terraform.State) error {
615+
rs, ok := state.RootModule().Resources[parent]
616+
if !ok {
617+
return fmt.Errorf("resource container not found: %s", parent)
618+
}
619+
620+
match, err := argon2id.ComparePasswordAndHash(password, rs.Primary.Attributes[key])
621+
if err != nil {
622+
return err
623+
}
624+
625+
if !match {
626+
return errors.New("password and hash do not match")
627+
}
628+
629+
return nil
630+
}
631+
}
632+
633+
func TestFilterSecretEnvsToPatch(t *testing.T) {
634+
testSecret := "test_secret"
635+
secretToDelete := "secret_to_delete"
636+
updatedSecret := "updated_secret"
637+
newSecret := "new_secret"
638+
639+
oldEnv := []*containerSDK.Secret{
640+
{Key: testSecret, Value: &testSecret},
641+
{Key: secretToDelete, Value: &secretToDelete},
642+
}
643+
newEnv := []*containerSDK.Secret{
644+
{Key: testSecret, Value: &updatedSecret},
645+
{Key: newSecret, Value: &newSecret},
646+
}
647+
648+
toPatch := container.FilterSecretEnvsToPatch(oldEnv, newEnv)
649+
assert.Equal(t, []*containerSDK.Secret{
650+
{Key: testSecret, Value: &updatedSecret},
651+
{Key: newSecret, Value: &newSecret},
652+
{Key: secretToDelete, Value: nil},
653+
}, toPatch)
654+
}

Diff for: internal/services/container/helpers_container.go

+37
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"slices"
78
"strings"
89
"time"
910

@@ -289,6 +290,19 @@ func flattenScalingOption(scalingOption *container.ContainerScalingOption) inter
289290
return flattenedScalingOption
290291
}
291292

293+
func flattenContainerSecrets(secrets []*container.SecretHashedValue) interface{} {
294+
if len(secrets) == 0 {
295+
return nil
296+
}
297+
298+
flattenedSecrets := make(map[string]interface{})
299+
for _, secret := range secrets {
300+
flattenedSecrets[secret.Key] = secret.HashedValue
301+
}
302+
303+
return flattenedSecrets
304+
}
305+
292306
func expandContainerSecrets(secretsRawMap interface{}) []*container.Secret {
293307
secretsMap := secretsRawMap.(map[string]interface{})
294308
secrets := make([]*container.Secret, 0, len(secretsMap))
@@ -358,3 +372,26 @@ func retryCreateContainerDomain(ctx context.Context, containerAPI *container.API
358372
}
359373
}
360374
}
375+
376+
func FilterSecretEnvsToPatch(oldEnv []*container.Secret, newEnv []*container.Secret) []*container.Secret {
377+
toPatch := []*container.Secret{}
378+
// create and update - ignore hashed values
379+
for _, env := range newEnv {
380+
if env.Value != nil && strings.HasPrefix(*env.Value, "$argon2id") {
381+
continue
382+
}
383+
384+
toPatch = append(toPatch, env)
385+
}
386+
387+
// delete
388+
for _, env := range oldEnv {
389+
if !slices.ContainsFunc(newEnv, func(s *container.Secret) bool {
390+
return s.Key == env.Key
391+
}) {
392+
toPatch = append(toPatch, &container.Secret{Key: env.Key, Value: nil})
393+
}
394+
}
395+
396+
return toPatch
397+
}

0 commit comments

Comments
 (0)