@@ -2,10 +2,13 @@ package gitlab
2
2
3
3
import (
4
4
"errors"
5
+ "fmt"
5
6
"log"
6
7
"net/http"
8
+ "net/url"
7
9
"strings"
8
10
11
+ retryablehttp "github.com/hashicorp/go-retryablehttp"
9
12
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
10
13
gitlab "github.com/xanzy/go-gitlab"
11
14
)
@@ -56,7 +59,9 @@ func resourceGitlabProjectVariable() *schema.Resource {
56
59
"environment_scope" : {
57
60
Type : schema .TypeString ,
58
61
Optional : true ,
59
- Default : false ,
62
+ Default : "*" ,
63
+ // Versions of GitLab prior to 13.4 cannot update environment_scope.
64
+ ForceNew : true ,
60
65
},
61
66
},
62
67
}
@@ -81,30 +86,55 @@ func resourceGitlabProjectVariableCreate(d *schema.ResourceData, meta interface{
81
86
Masked : & masked ,
82
87
EnvironmentScope : & environmentScope ,
83
88
}
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 )
85
93
86
94
_ , _ , err := client .ProjectVariables .CreateVariable (project , & options )
87
95
if err != nil {
88
96
return augmentProjectVariableClientError (d , err )
89
97
}
90
98
91
- d .SetId (buildTwoPartID ( & project , & key ) )
99
+ d .SetId (id )
92
100
93
101
return resourceGitlabProjectVariableRead (d , meta )
94
102
}
95
103
96
104
func resourceGitlabProjectVariableRead (d * schema.ResourceData , meta interface {}) error {
97
105
client := meta .(* gitlab.Client )
98
106
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 ())
102
127
}
103
128
104
- log .Printf ("[DEBUG] read gitlab project variable %s/%s " , project , key )
129
+ log .Printf ("[DEBUG] read gitlab project variable %q " , d . Id () )
105
130
106
- v , _ , err := client . ProjectVariables . GetVariable ( project , key )
131
+ v , err := getProjectVariable ( client , project , key , environmentScope )
107
132
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
+ }
108
138
return augmentProjectVariableClientError (d , err )
109
139
}
110
140
@@ -114,9 +144,6 @@ func resourceGitlabProjectVariableRead(d *schema.ResourceData, meta interface{})
114
144
d .Set ("project" , project )
115
145
d .Set ("protected" , v .Protected )
116
146
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.
120
147
d .Set ("environment_scope" , v .EnvironmentScope )
121
148
return nil
122
149
}
@@ -139,9 +166,9 @@ func resourceGitlabProjectVariableUpdate(d *schema.ResourceData, meta interface{
139
166
Masked : & masked ,
140
167
EnvironmentScope : & environmentScope ,
141
168
}
142
- log .Printf ("[DEBUG] update gitlab project variable %s/%s " , project , key )
169
+ log .Printf ("[DEBUG] update gitlab project variable %q " , d . Id () )
143
170
144
- _ , _ , err := client .ProjectVariables .UpdateVariable (project , key , options )
171
+ _ , _ , err := client .ProjectVariables .UpdateVariable (project , key , options , withEnvironmentScopeFilter ( environmentScope ) )
145
172
if err != nil {
146
173
return augmentProjectVariableClientError (d , err )
147
174
}
@@ -153,9 +180,14 @@ func resourceGitlabProjectVariableDelete(d *schema.ResourceData, meta interface{
153
180
client := meta .(* gitlab.Client )
154
181
project := d .Get ("project" ).(string )
155
182
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 ())
157
185
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 ))
159
191
return augmentProjectVariableClientError (d , err )
160
192
}
161
193
@@ -177,3 +209,43 @@ func isInvalidValueError(err error) bool {
177
209
strings .Contains (httpErr .Message , "value" ) &&
178
210
strings .Contains (httpErr .Message , "invalid" )
179
211
}
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