Skip to content

Commit 68c8c0e

Browse files
authored
gitlab_project_variable: fix nondeterministic reads when using environment_scope (#409)
* gitlab_project_variable: fix nondeterministic reads when using environment_scope * Respond to PR review - remove indirection to build and parse the resource id - link to issue in the resource docs - small test cleanup by making a isGitLabVersionLessThan SkipFunc
1 parent 2d6b08c commit 68c8c0e

File tree

4 files changed

+439
-224
lines changed

4 files changed

+439
-224
lines changed

gitlab/resource_gitlab_project_variable.go

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package gitlab
22

33
import (
44
"errors"
5+
"fmt"
56
"log"
67
"net/http"
8+
"net/url"
79
"strings"
810

11+
retryablehttp "github.com/hashicorp/go-retryablehttp"
912
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
1013
gitlab "github.com/xanzy/go-gitlab"
1114
)
@@ -56,7 +59,9 @@ func resourceGitlabProjectVariable() *schema.Resource {
5659
"environment_scope": {
5760
Type: schema.TypeString,
5861
Optional: true,
59-
Default: false,
62+
Default: "*",
63+
// Versions of GitLab prior to 13.4 cannot update environment_scope.
64+
ForceNew: true,
6065
},
6166
},
6267
}
@@ -81,30 +86,55 @@ func resourceGitlabProjectVariableCreate(d *schema.ResourceData, meta interface{
8186
Masked: &masked,
8287
EnvironmentScope: &environmentScope,
8388
}
84-
log.Printf("[DEBUG] create gitlab project variable %s/%s", project, key)
89+
90+
id := strings.Join([]string{project, key, environmentScope}, ":")
91+
92+
log.Printf("[DEBUG] create gitlab project variable %q", id)
8593

8694
_, _, err := client.ProjectVariables.CreateVariable(project, &options)
8795
if err != nil {
8896
return augmentProjectVariableClientError(d, err)
8997
}
9098

91-
d.SetId(buildTwoPartID(&project, &key))
99+
d.SetId(id)
92100

93101
return resourceGitlabProjectVariableRead(d, meta)
94102
}
95103

96104
func resourceGitlabProjectVariableRead(d *schema.ResourceData, meta interface{}) error {
97105
client := meta.(*gitlab.Client)
98106

99-
project, key, err := parseTwoPartID(d.Id())
100-
if err != nil {
101-
return err
107+
var (
108+
project string
109+
key string
110+
environmentScope string
111+
)
112+
113+
// An older version of this resource used the ID format "project:key".
114+
// For backwards compatibility we still support the old format.
115+
parts := strings.SplitN(d.Id(), ":", 4)
116+
switch len(parts) {
117+
case 2:
118+
project = parts[0]
119+
key = parts[1]
120+
environmentScope = d.Get("environment_scope").(string)
121+
case 3:
122+
project = parts[0]
123+
key = parts[1]
124+
environmentScope = parts[2]
125+
default:
126+
return fmt.Errorf(`Failed to parse project variable ID %q: expected format project:key or project:key:environment_scope`, d.Id())
102127
}
103128

104-
log.Printf("[DEBUG] read gitlab project variable %s/%s", project, key)
129+
log.Printf("[DEBUG] read gitlab project variable %q", d.Id())
105130

106-
v, _, err := client.ProjectVariables.GetVariable(project, key)
131+
v, err := getProjectVariable(client, project, key, environmentScope)
107132
if err != nil {
133+
if errors.Is(err, errProjectVariableNotExist) {
134+
log.Printf("[DEBUG] read gitlab project variable %q was not found", d.Id())
135+
d.SetId("")
136+
return nil
137+
}
108138
return augmentProjectVariableClientError(d, err)
109139
}
110140

@@ -114,9 +144,6 @@ func resourceGitlabProjectVariableRead(d *schema.ResourceData, meta interface{})
114144
d.Set("project", project)
115145
d.Set("protected", v.Protected)
116146
d.Set("masked", v.Masked)
117-
//For now I'm ignoring environment_scope when reading back data. (this can cause configuration drift so it is bad).
118-
//However I'm unable to stop terraform from gratuitously updating this to values that are unacceptable by Gitlab)
119-
//I don't have an enterprise license to properly test this either.
120147
d.Set("environment_scope", v.EnvironmentScope)
121148
return nil
122149
}
@@ -139,9 +166,9 @@ func resourceGitlabProjectVariableUpdate(d *schema.ResourceData, meta interface{
139166
Masked: &masked,
140167
EnvironmentScope: &environmentScope,
141168
}
142-
log.Printf("[DEBUG] update gitlab project variable %s/%s", project, key)
169+
log.Printf("[DEBUG] update gitlab project variable %q", d.Id())
143170

144-
_, _, err := client.ProjectVariables.UpdateVariable(project, key, options)
171+
_, _, err := client.ProjectVariables.UpdateVariable(project, key, options, withEnvironmentScopeFilter(environmentScope))
145172
if err != nil {
146173
return augmentProjectVariableClientError(d, err)
147174
}
@@ -153,9 +180,14 @@ func resourceGitlabProjectVariableDelete(d *schema.ResourceData, meta interface{
153180
client := meta.(*gitlab.Client)
154181
project := d.Get("project").(string)
155182
key := d.Get("key").(string)
156-
log.Printf("[DEBUG] Delete gitlab project variable %s/%s", project, key)
183+
environmentScope := d.Get("environment_scope").(string)
184+
log.Printf("[DEBUG] Delete gitlab project variable %q", d.Id())
157185

158-
_, err := client.ProjectVariables.RemoveVariable(project, key)
186+
// Note that the environment_scope filter is added here to support GitLab versions >= 13.4,
187+
// but it will be ignored in prior versions, causing nondeterministic destroy behavior when
188+
// destroying or updating scoped variables.
189+
// ref: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39209
190+
_, err := client.ProjectVariables.RemoveVariable(project, key, withEnvironmentScopeFilter(environmentScope))
159191
return augmentProjectVariableClientError(d, err)
160192
}
161193

@@ -177,3 +209,43 @@ func isInvalidValueError(err error) bool {
177209
strings.Contains(httpErr.Message, "value") &&
178210
strings.Contains(httpErr.Message, "invalid")
179211
}
212+
213+
func withEnvironmentScopeFilter(environmentScope string) gitlab.RequestOptionFunc {
214+
return func(req *retryablehttp.Request) error {
215+
query, err := url.ParseQuery(req.Request.URL.RawQuery)
216+
if err != nil {
217+
return err
218+
}
219+
query.Set("filter[environment_scope]", environmentScope)
220+
req.Request.URL.RawQuery = query.Encode()
221+
return nil
222+
}
223+
}
224+
225+
var errProjectVariableNotExist = errors.New("project variable does not exist")
226+
227+
func getProjectVariable(client *gitlab.Client, project interface{}, key, environmentScope string) (*gitlab.ProjectVariable, error) {
228+
// List and filter variables manually to support GitLab versions < v13.4 (2020-08-22)
229+
// ref: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39209
230+
231+
page := 1
232+
233+
for {
234+
projectVariables, resp, err := client.ProjectVariables.ListVariables(project, &gitlab.ListProjectVariablesOptions{Page: page})
235+
if err != nil {
236+
return nil, err
237+
}
238+
239+
for _, v := range projectVariables {
240+
if v.Key == key && v.EnvironmentScope == environmentScope {
241+
return v, nil
242+
}
243+
}
244+
245+
if resp.NextPage == 0 {
246+
return nil, errProjectVariableNotExist
247+
}
248+
249+
page++
250+
}
251+
}

0 commit comments

Comments
 (0)