@@ -3,11 +3,15 @@ package provider
3
3
import (
4
4
"context"
5
5
"encoding/base64"
6
+ "errors"
6
7
"fmt"
7
8
"log"
9
+ "net/http"
8
10
"strings"
11
+ "time"
9
12
10
13
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
14
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
11
15
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12
16
gitlab "github.com/xanzy/go-gitlab"
13
17
)
@@ -31,6 +35,13 @@ var _ = registerResource("gitlab_repository_file", func() *schema.Resource {
31
35
return & schema.Resource {
32
36
Description : `The ` + "`gitlab_repository_file`" + ` resource allows to manage the lifecycle of a file within a repository.
33
37
38
+ -> **Timeouts** Default timeout for *Create*, *Update* and *Delete* is one minute and can be configured in the ` + "`timeouts`" + ` block.
39
+
40
+ -> **Implementation Detail** GitLab is unable to handle concurrent calls to the GitLab repository files API for the same project.
41
+ Therefore, this resource queues every call to the repository files API no matter of the project, which may slow down the terraform
42
+ execution time for some configurations. In addition, retries are performed in case a refresh is required because another application
43
+ changed the repository at the same time.
44
+
34
45
**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/repository_files.html)` ,
35
46
36
47
CreateContext : resourceGitlabRepositoryFileCreate ,
@@ -40,6 +51,11 @@ var _ = registerResource("gitlab_repository_file", func() *schema.Resource {
40
51
Importer : & schema.ResourceImporter {
41
52
StateContext : schema .ImportStatePassthroughContext ,
42
53
},
54
+ Timeouts : & schema.ResourceTimeout {
55
+ Create : schema .DefaultTimeout (1 * time .Minute ),
56
+ Update : schema .DefaultTimeout (1 * time .Minute ),
57
+ Delete : schema .DefaultTimeout (1 * time .Minute ),
58
+ },
43
59
44
60
// the schema matches https://docs.gitlab.com/ee/api/repository_files.html#create-new-file-in-repository
45
61
// However, we don't support the `encoding` parameter as it seems to be broken.
@@ -104,12 +120,22 @@ func resourceGitlabRepositoryFileCreate(ctx context.Context, d *schema.ResourceD
104
120
options .StartBranch = gitlab .String (startBranch .(string ))
105
121
}
106
122
107
- repositoryFile , _ , err := client .RepositoryFiles .CreateFile (project , filePath , options , gitlab .WithContext (ctx ))
123
+ err := resource .RetryContext (ctx , d .Timeout (schema .TimeoutCreate ), func () * resource.RetryError {
124
+ repositoryFile , _ , err := client .RepositoryFiles .CreateFile (project , filePath , options , gitlab .WithContext (ctx ))
125
+ if err != nil {
126
+ if isRefreshError (err ) {
127
+ return resource .RetryableError (err )
128
+ }
129
+ return resource .NonRetryableError (err )
130
+ }
131
+
132
+ d .SetId (resourceGitLabRepositoryFileBuildId (project , repositoryFile .Branch , repositoryFile .FilePath ))
133
+ return nil
134
+ })
108
135
if err != nil {
109
136
return diag .FromErr (err )
110
137
}
111
138
112
- d .SetId (resourceGitLabRepositoryFileBuildId (project , repositoryFile .Branch , repositoryFile .FilePath ))
113
139
return resourceGitlabRepositoryFileRead (ctx , d , meta )
114
140
}
115
141
@@ -163,25 +189,36 @@ func resourceGitlabRepositoryFileUpdate(ctx context.Context, d *schema.ResourceD
163
189
Ref : gitlab .String (branch ),
164
190
}
165
191
166
- existingRepositoryFile , _ , err := client .RepositoryFiles .GetFile (project , filePath , readOptions , gitlab .WithContext (ctx ))
167
- if err != nil {
168
- return diag .FromErr (err )
169
- }
170
-
171
- options := & gitlab.UpdateFileOptions {
192
+ updateOptions := & gitlab.UpdateFileOptions {
172
193
Branch : gitlab .String (branch ),
173
194
Encoding : gitlab .String (encoding ),
174
195
AuthorEmail : gitlab .String (d .Get ("author_email" ).(string )),
175
196
AuthorName : gitlab .String (d .Get ("author_name" ).(string )),
176
197
Content : gitlab .String (d .Get ("content" ).(string )),
177
198
CommitMessage : gitlab .String (d .Get ("commit_message" ).(string )),
178
- LastCommitID : gitlab .String (existingRepositoryFile .LastCommitID ),
179
199
}
180
200
if startBranch , ok := d .GetOk ("start_branch" ); ok {
181
- options .StartBranch = gitlab .String (startBranch .(string ))
201
+ updateOptions .StartBranch = gitlab .String (startBranch .(string ))
182
202
}
183
203
184
- _ , _ , err = client .RepositoryFiles .UpdateFile (project , filePath , options , gitlab .WithContext (ctx ))
204
+ err = resource .RetryContext (ctx , d .Timeout (schema .TimeoutUpdate ), func () * resource.RetryError {
205
+ // NOTE: we also re-read the file to obtain an eventually changed `LastCommitID` for which we needed the refresh
206
+ existingRepositoryFile , _ , err := client .RepositoryFiles .GetFile (project , filePath , readOptions , gitlab .WithContext (ctx ))
207
+ if err != nil {
208
+ return resource .NonRetryableError (err )
209
+ }
210
+
211
+ updateOptions .LastCommitID = gitlab .String (existingRepositoryFile .LastCommitID )
212
+ _ , _ , err = client .RepositoryFiles .UpdateFile (project , filePath , updateOptions , gitlab .WithContext (ctx ))
213
+ if err != nil {
214
+ if isRefreshError (err ) {
215
+ return resource .RetryableError (err )
216
+ }
217
+ return resource .NonRetryableError (err )
218
+ }
219
+
220
+ return nil
221
+ })
185
222
if err != nil {
186
223
return diag .FromErr (err )
187
224
}
@@ -207,23 +244,33 @@ func resourceGitlabRepositoryFileDelete(ctx context.Context, d *schema.ResourceD
207
244
readOptions := & gitlab.GetFileOptions {
208
245
Ref : gitlab .String (branch ),
209
246
}
210
-
211
- existingRepositoryFile , _ , err := client .RepositoryFiles .GetFile (project , filePath , readOptions , gitlab .WithContext (ctx ))
212
- if err != nil {
213
- return diag .FromErr (err )
214
- }
215
-
216
- options := & gitlab.DeleteFileOptions {
247
+ deleteOptions := & gitlab.DeleteFileOptions {
217
248
Branch : gitlab .String (d .Get ("branch" ).(string )),
218
249
AuthorEmail : gitlab .String (d .Get ("author_email" ).(string )),
219
250
AuthorName : gitlab .String (d .Get ("author_name" ).(string )),
220
251
CommitMessage : gitlab .String (fmt .Sprintf ("[DELETE]: %s" , d .Get ("commit_message" ).(string ))),
221
- LastCommitID : gitlab .String (existingRepositoryFile .LastCommitID ),
222
252
}
223
253
224
- resp , err := client .RepositoryFiles .DeleteFile (project , filePath , options )
254
+ err = resource .RetryContext (ctx , d .Timeout (schema .TimeoutDelete ), func () * resource.RetryError {
255
+ // NOTE: we also re-read the file to obtain an eventually changed `LastCommitID` for which we needed the refresh
256
+
257
+ existingRepositoryFile , _ , err := client .RepositoryFiles .GetFile (project , filePath , readOptions , gitlab .WithContext (ctx ))
258
+ if err != nil {
259
+ return resource .NonRetryableError (err )
260
+ }
261
+
262
+ deleteOptions .LastCommitID = gitlab .String (existingRepositoryFile .LastCommitID )
263
+ resp , err := client .RepositoryFiles .DeleteFile (project , filePath , deleteOptions )
264
+ if err != nil {
265
+ if isRefreshError (err ) {
266
+ return resource .RetryableError (err )
267
+ }
268
+ return resource .NonRetryableError (fmt .Errorf ("%s failed to delete repository file: (%s) %v" , d .Id (), resp .Status , err ))
269
+ }
270
+ return nil
271
+ })
225
272
if err != nil {
226
- return diag .Errorf ( "%s failed to delete repository file: (%s) %v" , d . Id (), resp . Status , err )
273
+ return diag .FromErr ( err )
227
274
}
228
275
229
276
return nil
@@ -249,3 +296,10 @@ func resourceGitLabRepositoryFileParseId(id string) (string, string, string, err
249
296
func resourceGitLabRepositoryFileBuildId (project string , branch string , filePath string ) string {
250
297
return fmt .Sprintf ("%s:%s:%s" , project , branch , filePath )
251
298
}
299
+
300
+ func isRefreshError (err error ) bool {
301
+ var httpErr * gitlab.ErrorResponse
302
+ return errors .As (err , & httpErr ) &&
303
+ httpErr .Response .StatusCode == http .StatusBadRequest &&
304
+ strings .Contains (httpErr .Message , "Please refresh and try again" )
305
+ }
0 commit comments