Skip to content

Commit e189068

Browse files
committed
resource/gitlab_repository_file: Implement explicit retries for refresh errors
Closes #940
1 parent e8bd2c1 commit e189068

File tree

2 files changed

+97
-21
lines changed

2 files changed

+97
-21
lines changed

docs/resources/repository_file.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@ page_title: "gitlab_repository_file Resource - terraform-provider-gitlab"
44
subcategory: ""
55
description: |-
66
The gitlab_repository_file resource allows to manage the lifecycle of a file within a repository.
7+
-> Timeouts Default timeout for Create, Update and Delete is one minute and can be configured in the timeouts block.
8+
-> Implementation Detail GitLab is unable to handle concurrent calls to the GitLab repository files API for the same project.
9+
Therefore, this resource queues every call to the repository files API no matter of the project, which may slow down the terraform
10+
execution time for some configurations. In addition, retries are performed in case a refresh is required because another application
11+
changed the repository at the same time.
712
Upstream API: GitLab REST API docs https://docs.gitlab.com/ee/api/repository_files.html
813
---
914

1015
# gitlab_repository_file (Resource)
1116

1217
The `gitlab_repository_file` resource allows to manage the lifecycle of a file within a repository.
1318

19+
-> **Timeouts** Default timeout for *Create*, *Update* and *Delete* is one minute and can be configured in the `timeouts` block.
20+
21+
-> **Implementation Detail** GitLab is unable to handle concurrent calls to the GitLab repository files API for the same project.
22+
Therefore, this resource queues every call to the repository files API no matter of the project, which may slow down the terraform
23+
execution time for some configurations. In addition, retries are performed in case a refresh is required because another application
24+
changed the repository at the same time.
25+
1426
**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/repository_files.html)
1527

1628
## Example Usage
@@ -54,6 +66,7 @@ resource "gitlab_repository_file" "this" {
5466
- `author_name` (String) Name of the commit author.
5567
- `id` (String) The ID of this resource.
5668
- `start_branch` (String) Name of the branch to start the new commit from.
69+
- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))
5770

5871
### Read-Only
5972

@@ -66,6 +79,15 @@ resource "gitlab_repository_file" "this" {
6679
- `ref` (String) The name of branch, tag or commit.
6780
- `size` (Number) The file size.
6881

82+
<a id="nestedblock--timeouts"></a>
83+
### Nested Schema for `timeouts`
84+
85+
Optional:
86+
87+
- `create` (String)
88+
- `delete` (String)
89+
- `update` (String)
90+
6991
## Import
7092

7193
Import is supported using the following syntax:

internal/provider/resource_gitlab_repository_file.go

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ package provider
33
import (
44
"context"
55
"encoding/base64"
6+
"errors"
67
"fmt"
78
"log"
9+
"net/http"
810
"strings"
11+
"time"
912

1013
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
14+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1115
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1216
gitlab "github.com/xanzy/go-gitlab"
1317
)
@@ -31,6 +35,13 @@ var _ = registerResource("gitlab_repository_file", func() *schema.Resource {
3135
return &schema.Resource{
3236
Description: `The ` + "`gitlab_repository_file`" + ` resource allows to manage the lifecycle of a file within a repository.
3337
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+
3445
**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/repository_files.html)`,
3546

3647
CreateContext: resourceGitlabRepositoryFileCreate,
@@ -40,6 +51,11 @@ var _ = registerResource("gitlab_repository_file", func() *schema.Resource {
4051
Importer: &schema.ResourceImporter{
4152
StateContext: schema.ImportStatePassthroughContext,
4253
},
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+
},
4359

4460
// the schema matches https://docs.gitlab.com/ee/api/repository_files.html#create-new-file-in-repository
4561
// 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
104120
options.StartBranch = gitlab.String(startBranch.(string))
105121
}
106122

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+
})
108135
if err != nil {
109136
return diag.FromErr(err)
110137
}
111138

112-
d.SetId(resourceGitLabRepositoryFileBuildId(project, repositoryFile.Branch, repositoryFile.FilePath))
113139
return resourceGitlabRepositoryFileRead(ctx, d, meta)
114140
}
115141

@@ -163,25 +189,36 @@ func resourceGitlabRepositoryFileUpdate(ctx context.Context, d *schema.ResourceD
163189
Ref: gitlab.String(branch),
164190
}
165191

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{
172193
Branch: gitlab.String(branch),
173194
Encoding: gitlab.String(encoding),
174195
AuthorEmail: gitlab.String(d.Get("author_email").(string)),
175196
AuthorName: gitlab.String(d.Get("author_name").(string)),
176197
Content: gitlab.String(d.Get("content").(string)),
177198
CommitMessage: gitlab.String(d.Get("commit_message").(string)),
178-
LastCommitID: gitlab.String(existingRepositoryFile.LastCommitID),
179199
}
180200
if startBranch, ok := d.GetOk("start_branch"); ok {
181-
options.StartBranch = gitlab.String(startBranch.(string))
201+
updateOptions.StartBranch = gitlab.String(startBranch.(string))
182202
}
183203

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+
})
185222
if err != nil {
186223
return diag.FromErr(err)
187224
}
@@ -207,23 +244,33 @@ func resourceGitlabRepositoryFileDelete(ctx context.Context, d *schema.ResourceD
207244
readOptions := &gitlab.GetFileOptions{
208245
Ref: gitlab.String(branch),
209246
}
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{
217248
Branch: gitlab.String(d.Get("branch").(string)),
218249
AuthorEmail: gitlab.String(d.Get("author_email").(string)),
219250
AuthorName: gitlab.String(d.Get("author_name").(string)),
220251
CommitMessage: gitlab.String(fmt.Sprintf("[DELETE]: %s", d.Get("commit_message").(string))),
221-
LastCommitID: gitlab.String(existingRepositoryFile.LastCommitID),
222252
}
223253

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+
})
225272
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)
227274
}
228275

229276
return nil
@@ -249,3 +296,10 @@ func resourceGitLabRepositoryFileParseId(id string) (string, string, string, err
249296
func resourceGitLabRepositoryFileBuildId(project string, branch string, filePath string) string {
250297
return fmt.Sprintf("%s:%s:%s", project, branch, filePath)
251298
}
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

Comments
 (0)