Skip to content

Commit 2267e5f

Browse files
authored
Fix a bunch of bugs in resource_project_environment_variables (#306)
* Fix a bunch of bugs in resource_project_environment_variables - Refresh state logic was broken - We didn't factor in existing env vars properly when doing an Update - It was fetching encrypted env vars, not decrypted ones - Conflict errors weren't being handled properly in the client - Decrypted field isn't always there in the API response - Deleting env vars is eventually consistent - Update was trying to update every env var, not just the new/changed ones This closes #277 and #262 (probably) * Set newly created env vars to be flagged as not decypted
1 parent 1747cec commit 2267e5f

File tree

3 files changed

+167
-59
lines changed

3 files changed

+167
-59
lines changed

client/environment_variable.go

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (c *Client) CreateEnvironmentVariable(ctx context.Context, request CreateEn
4949
if err2 != nil {
5050
return e, err2
5151
}
52-
envs, err3 := c.ListEnvironmentVariables(ctx, request.TeamID, request.ProjectID)
52+
envs, err3 := c.GetEnvironmentVariables(ctx, request.ProjectID, request.TeamID)
5353
if err3 != nil {
5454
return e, fmt.Errorf("%s: unable to list environment variables to detect conflict: %s", err, err3)
5555
}
@@ -67,23 +67,6 @@ func (c *Client) CreateEnvironmentVariable(ctx context.Context, request CreateEn
6767
return response.Created, err
6868
}
6969

70-
func (c *Client) ListEnvironmentVariables(ctx context.Context, teamID, projectID string) (envs []EnvironmentVariable, err error) {
71-
url := fmt.Sprintf("%s/v10/projects/%s/env", c.baseURL, projectID)
72-
if c.teamID(teamID) != "" {
73-
url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID))
74-
}
75-
76-
response := struct {
77-
Envs []EnvironmentVariable `json:"envs"`
78-
}{}
79-
err = c.doRequest(clientRequest{
80-
ctx: ctx,
81-
method: "GET",
82-
url: url,
83-
}, &response)
84-
return response.Envs, err
85-
}
86-
8770
func overlaps(s []string, e []string) bool {
8871
set := make(map[string]struct{}, len(s))
8972
for _, a := range s {
@@ -179,8 +162,14 @@ func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateE
179162
return nil, fmt.Errorf("%w - %s", err, payload)
180163
}
181164

165+
decrypted := false
166+
for i := 0; i < len(response.Created); i++ {
167+
// When env vars are created, their values are encrypted
168+
response.Created[i].Decrypted = &decrypted
169+
}
170+
182171
if len(response.Failed) > 0 {
183-
envs, err := c.ListEnvironmentVariables(ctx, request.TeamID, request.ProjectID)
172+
envs, err := c.GetEnvironmentVariables(ctx, request.ProjectID, request.TeamID)
184173
if err != nil {
185174
return response.Created, fmt.Errorf("failed to create environment variables. error detecting conflicting environment variables: %w", err)
186175
}
@@ -193,13 +182,11 @@ func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateE
193182
}, envs)
194183
if found {
195184
err = fmt.Errorf("%w, conflicting environment variable ID is %s", err, id)
185+
} else {
186+
err = fmt.Errorf("failed to create environment variables, %s", failed.Error.Message)
196187
}
197188
} else {
198-
key := ""
199-
if failed.Error.Key != nil {
200-
key = *failed.Error.Key
201-
}
202-
err = fmt.Errorf("failed to create environment variables, %s %s %s", failed.Error.Message, key, failed.Error.Target)
189+
err = fmt.Errorf("failed to create environment variables, %s", failed.Error.Message)
203190
}
204191
}
205192
return response.Created, err

client/project.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type EnvironmentVariable struct {
3232
ID string `json:"id,omitempty"`
3333
TeamID string `json:"-"`
3434
Comment string `json:"comment"`
35-
Decrypted bool `json:"decrypted"`
35+
Decrypted *bool `json:"decrypted"`
3636
}
3737

3838
type DeploymentExpiration struct {

vercel/resource_project_environment_variables.go

Lines changed: 155 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package vercel
33
import (
44
"context"
55
"fmt"
6+
"time"
67

78
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
89
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
@@ -161,7 +162,7 @@ type ProjectEnvironmentVariables struct {
161162
Variables types.Set `tfsdk:"variables"`
162163
}
163164

164-
func (p *ProjectEnvironmentVariables) environment(ctx context.Context) ([]EnvironmentItem, diag.Diagnostics) {
165+
func (p *ProjectEnvironmentVariables) environment(ctx context.Context) (EnvironmentItems, diag.Diagnostics) {
165166
if p.Variables.IsNull() {
166167
return nil, nil
167168
}
@@ -239,44 +240,41 @@ func (r *projectEnvironmentVariablesResource) ModifyPlan(ctx context.Context, re
239240
}
240241
}
241242

242-
func (e *ProjectEnvironmentVariables) toCreateEnvironmentVariablesRequest(ctx context.Context) (r client.CreateEnvironmentVariablesRequest, diags diag.Diagnostics) {
243-
envs, diags := e.environment(ctx)
244-
if diags.HasError() {
245-
return r, diags
246-
}
243+
type EnvironmentItems []EnvironmentItem
247244

245+
func (e *EnvironmentItems) toCreateEnvironmentVariablesRequest(ctx context.Context, projectID types.String, teamID types.String) (r client.CreateEnvironmentVariablesRequest, diags diag.Diagnostics) {
248246
variables := []client.EnvironmentVariableRequest{}
249-
for _, e := range envs {
247+
for _, env := range *e {
250248
var target []string
251-
diags = e.Target.ElementsAs(ctx, &target, true)
249+
diags = env.Target.ElementsAs(ctx, &target, true)
252250
if diags.HasError() {
253251
return r, diags
254252
}
255253
var customEnvironmentIDs []string
256-
diags = e.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true)
254+
diags = env.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true)
257255
if diags.HasError() {
258256
return r, diags
259257
}
260258
var envVariableType string
261-
if e.Sensitive.ValueBool() {
259+
if env.Sensitive.ValueBool() {
262260
envVariableType = "sensitive"
263261
} else {
264262
envVariableType = "encrypted"
265263
}
266264
variables = append(variables, client.EnvironmentVariableRequest{
267-
Key: e.Key.ValueString(),
268-
Value: e.Value.ValueString(),
265+
Key: env.Key.ValueString(),
266+
Value: env.Value.ValueString(),
269267
Target: target,
270268
CustomEnvironmentIDs: customEnvironmentIDs,
271269
Type: envVariableType,
272-
GitBranch: e.GitBranch.ValueStringPointer(),
273-
Comment: e.Comment.ValueString(),
270+
GitBranch: env.GitBranch.ValueStringPointer(),
271+
Comment: env.Comment.ValueString(),
274272
})
275273
}
276274

277275
return client.CreateEnvironmentVariablesRequest{
278-
ProjectID: e.ProjectID.ValueString(),
279-
TeamID: e.TeamID.ValueString(),
276+
ProjectID: projectID.ValueString(),
277+
TeamID: teamID.ValueString(),
280278
EnvironmentVariables: variables,
281279
}, nil
282280
}
@@ -323,7 +321,7 @@ func convertResponseToProjectEnvironmentVariables(
323321
if e.Type == "sensitive" {
324322
value = types.StringNull()
325323
}
326-
if !e.Decrypted || e.Type == "sensitive" {
324+
if e.Decrypted != nil && !*e.Decrypted || e.Type == "sensitive" {
327325
for _, p := range environment {
328326
var target []string
329327
diags := p.Target.ElementsAs(ctx, &target, true)
@@ -393,7 +391,13 @@ func (r *projectEnvironmentVariablesResource) Create(ctx context.Context, req re
393391
return
394392
}
395393

396-
request, diags := plan.toCreateEnvironmentVariablesRequest(ctx)
394+
envs, diags := plan.environment(ctx)
395+
if diags.HasError() {
396+
resp.Diagnostics.Append(diags...)
397+
return
398+
}
399+
400+
request, diags := envs.toCreateEnvironmentVariablesRequest(ctx, plan.ProjectID, plan.TeamID)
397401
if diags.HasError() {
398402
resp.Diagnostics.Append(diags...)
399403
return
@@ -442,7 +446,7 @@ func (r *projectEnvironmentVariablesResource) Read(ctx context.Context, req reso
442446
}
443447
existingIDs := map[string]struct{}{}
444448
for _, e := range existing {
445-
if e.ID.ValueString() == "" {
449+
if e.ID.ValueString() != "" {
446450
existingIDs[e.ID.ValueString()] = struct{}{}
447451
}
448452
}
@@ -451,7 +455,7 @@ func (r *projectEnvironmentVariablesResource) Read(ctx context.Context, req reso
451455
return
452456
}
453457

454-
envs, err := r.client.ListEnvironmentVariables(ctx, state.TeamID.ValueString(), state.ProjectID.ValueString())
458+
envs, err := r.client.GetEnvironmentVariables(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString())
455459
if client.NotFound(err) {
456460
resp.State.RemoveResource(ctx)
457461
return
@@ -467,9 +471,34 @@ func (r *projectEnvironmentVariablesResource) Read(ctx context.Context, req reso
467471
var toUse []client.EnvironmentVariable
468472
for _, e := range envs {
469473
if _, ok := existingIDs[e.ID]; ok {
474+
// This ID exists in the env vars we have already. So use it.
470475
toUse = append(toUse, e)
471476
}
472477
}
478+
for _, e := range envs {
479+
if _, ok := existingIDs[e.ID]; !ok {
480+
// The env var exists at the moment, but not in TF state (the ID isn't present).
481+
// Check if it has the same `key`, `target` and `custom_environment_ids` as an existing env var.
482+
// This detects drift for stuff like: deleting an env var and then creating it again (the ID changes).
483+
for _, ee := range existing {
484+
var target []string
485+
diags := ee.Target.ElementsAs(ctx, &target, true)
486+
if diags.HasError() {
487+
resp.Diagnostics.Append(diags...)
488+
return
489+
}
490+
var customEnvironmentIDs []string
491+
diags = ee.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true)
492+
if diags.HasError() {
493+
resp.Diagnostics.Append(diags...)
494+
return
495+
}
496+
if ee.Key.ValueString() == e.Key && isSameStringSet(target, e.Target) && isSameStringSet(customEnvironmentIDs, e.CustomEnvironmentIDs) {
497+
toUse = append(toUse, e)
498+
}
499+
}
500+
}
501+
}
473502

474503
result, diags := convertResponseToProjectEnvironmentVariables(ctx, toUse, state, nil)
475504
if diags.HasError() {
@@ -516,7 +545,7 @@ func (r *projectEnvironmentVariablesResource) Update(ctx context.Context, req re
516545
return
517546
}
518547
plannedEnvsByID := map[string]EnvironmentItem{}
519-
toAdd := []EnvironmentItem{}
548+
var toAdd EnvironmentItems
520549
for _, e := range planEnvs {
521550
if e.ID.ValueString() != "" {
522551
plannedEnvsByID[e.ID.ValueString()] = e
@@ -525,8 +554,8 @@ func (r *projectEnvironmentVariablesResource) Update(ctx context.Context, req re
525554
}
526555
}
527556

528-
var toRemove []EnvironmentItem
529-
var unchanged []EnvironmentItem
557+
var toRemove EnvironmentItems
558+
var unchanged EnvironmentItems
530559
for _, e := range stateEnvs {
531560
plannedEnv, ok := plannedEnvsByID[e.ID.ValueString()]
532561
if !ok {
@@ -541,11 +570,105 @@ func (r *projectEnvironmentVariablesResource) Update(ctx context.Context, req re
541570
unchanged = append(unchanged, e)
542571
}
543572

544-
tflog.Info(ctx, "Removing environment variables", map[string]any{"to_remove": toRemove})
545-
tflog.Info(ctx, "Adding environment variables", map[string]any{"to_add": toAdd})
573+
envsFromAPI, err := r.client.GetEnvironmentVariables(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString())
574+
if client.NotFound(err) {
575+
resp.State.RemoveResource(ctx)
576+
return
577+
}
578+
if err != nil {
579+
resp.Diagnostics.AddError(
580+
"Error reading project environment variables as part of environment variable update",
581+
"Could not read environment variables as part of updating, unexpected error: "+err.Error(),
582+
)
583+
return
584+
}
585+
skipAdding := map[int]bool{}
586+
for _, e := range envsFromAPI {
587+
// The env var exists at the moment, but not in TF state (the ID isn't present).
588+
// Check if it has the same `key`, `target` and `custom_environment_ids` and value as any env var we are adding.
589+
// This detects drift for stuff like: deleting an env var and then creating it again (the ID changes, but
590+
// nothing else).
591+
if _, ok := plannedEnvsByID[e.ID]; !ok { // env isn't in the planned envs
592+
for i, ee := range toAdd { // look for a matching env var in the toAdd list
593+
var target []string
594+
diags := ee.Target.ElementsAs(ctx, &target, true)
595+
if diags.HasError() {
596+
resp.Diagnostics.Append(diags...)
597+
return
598+
}
599+
var customEnvironmentIDs []string
600+
diags = ee.CustomEnvironmentIDs.ElementsAs(ctx, &customEnvironmentIDs, true)
601+
if diags.HasError() {
602+
resp.Diagnostics.Append(diags...)
603+
return
604+
}
605+
if ee.Key.ValueString() == e.Key && isSameStringSet(target, e.Target) && isSameStringSet(customEnvironmentIDs, e.CustomEnvironmentIDs) {
606+
if e.Decrypted != nil && !*e.Decrypted {
607+
continue // We don't know if it's value is encrypted.
608+
}
609+
if e.Type == "sensitive" {
610+
continue // We don't know if it's the same env var if sensitive
611+
}
612+
if e.Value != ee.Value.ValueString() {
613+
continue // Value mismatches, so we need to update it.
614+
}
615+
616+
var targetValue types.Set
617+
if len(e.Target) > 0 {
618+
target := make([]attr.Value, 0, len(e.Target))
619+
for _, t := range e.Target {
620+
target = append(target, types.StringValue(t))
621+
}
622+
targetValue = types.SetValueMust(types.StringType, target)
623+
} else {
624+
targetValue = types.SetNull(types.StringType)
625+
}
626+
627+
var customEnvIDsValue types.Set
628+
if len(e.CustomEnvironmentIDs) > 0 {
629+
customEnvIDs := make([]attr.Value, 0, len(e.CustomEnvironmentIDs))
630+
for _, c := range e.CustomEnvironmentIDs {
631+
customEnvIDs = append(customEnvIDs, types.StringValue(c))
632+
}
633+
customEnvIDsValue = types.SetValueMust(types.StringType, customEnvIDs)
634+
} else {
635+
customEnvIDsValue = types.SetNull(types.StringType)
636+
}
637+
unchanged = append(unchanged, EnvironmentItem{
638+
Key: types.StringValue(e.Key),
639+
Value: types.StringValue(e.Value),
640+
Target: targetValue,
641+
CustomEnvironmentIDs: customEnvIDsValue,
642+
GitBranch: types.StringPointerValue(e.GitBranch),
643+
ID: types.StringValue(e.ID),
644+
Sensitive: types.BoolValue(e.Type == "sensitive"),
645+
Comment: types.StringValue(e.Comment),
646+
})
647+
skipAdding[i] = true
648+
}
649+
}
650+
}
651+
}
652+
var filteredToAdd EnvironmentItems
653+
for i, e := range toAdd {
654+
if _, ok := skipAdding[i]; ok {
655+
continue
656+
}
657+
filteredToAdd = append(filteredToAdd, e)
658+
}
659+
toAdd = filteredToAdd
660+
661+
tflog.Info(ctx, "Updating environment variables", map[string]any{
662+
"to_remove": len(toRemove),
663+
"to_add": len(toAdd),
664+
"unchanged": len(unchanged),
665+
})
546666

547667
for _, v := range toRemove {
548668
err := r.client.DeleteEnvironmentVariable(ctx, state.ProjectID.ValueString(), state.TeamID.ValueString(), v.ID.ValueString())
669+
if client.NotFound(err) {
670+
continue
671+
}
549672
if err != nil {
550673
resp.Diagnostics.AddError(
551674
"Error updating Project Environment Variables",
@@ -566,16 +689,18 @@ func (r *projectEnvironmentVariablesResource) Update(ctx context.Context, req re
566689
}
567690

568691
var response []client.EnvironmentVariable
569-
var err error
570692
if len(toAdd) > 0 {
571-
request, diags := plan.toCreateEnvironmentVariablesRequest(ctx)
693+
if len(toRemove) > 0 {
694+
// Sleep a bit to ensure the environment variables are fully propagated before we try to create them
695+
// This is disgusting, but what you gonna do?
696+
time.Sleep(time.Second * 5)
697+
}
698+
request, diags := toAdd.toCreateEnvironmentVariablesRequest(ctx, plan.ProjectID, plan.TeamID)
699+
572700
if diags.HasError() {
573701
resp.Diagnostics.Append(diags...)
574702
return
575703
}
576-
tflog.Info(ctx, "create request", map[string]any{
577-
"request": request,
578-
})
579704
response, err = r.client.CreateEnvironmentVariables(ctx, request)
580705
if err != nil {
581706
resp.Diagnostics.AddError(
@@ -586,10 +711,6 @@ func (r *projectEnvironmentVariablesResource) Update(ctx context.Context, req re
586711
}
587712
}
588713

589-
tflog.Info(ctx, "project env var response", map[string]any{
590-
"response": response,
591-
})
592-
593714
result, diags := convertResponseToProjectEnvironmentVariables(ctx, response, plan, unchanged)
594715
if diags.HasError() {
595716
resp.Diagnostics.Append(diags...)

0 commit comments

Comments
 (0)