Skip to content

Commit e8bd2c1

Browse files
committed
resource/gitlab_repository_file: Implement syncing when accessing API
Closes #940
1 parent 0322d78 commit e8bd2c1

File tree

4 files changed

+119
-8
lines changed

4 files changed

+119
-8
lines changed

docs/resources/repository_file.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@ 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-
~> Limitations: The GitLab Repository Files API https://docs.gitlab.com/ee/api/repository_files.html can only create, update or delete a single file at the time. The API will also fail with a 400 https://docs.gitlab.com/ee/api/repository_files.html#update-existing-file-in-repository response status code if the underlying repository is changed while the API tries to make changes. Therefore, it's recommended to make sure that you execute it with -parallelism=1 https://www.terraform.io/docs/cli/commands/apply.html#parallelism-n and that no other entity than the terraform at hand makes changes to the underlying repository while it's executing.
87
Upstream API: GitLab REST API docs https://docs.gitlab.com/ee/api/repository_files.html
98
---
109

1110
# gitlab_repository_file (Resource)
1211

1312
The `gitlab_repository_file` resource allows to manage the lifecycle of a file within a repository.
1413

15-
~> **Limitations**: The [GitLab Repository Files API](https://docs.gitlab.com/ee/api/repository_files.html) can only create, update or delete a single file at the time. The API will also [fail with a 400](https://docs.gitlab.com/ee/api/repository_files.html#update-existing-file-in-repository) response status code if the underlying repository is changed while the API tries to make changes. Therefore, it's recommended to make sure that you execute it with [-parallelism=1](https://www.terraform.io/docs/cli/commands/apply.html#parallelism-n) and that no other entity than the terraform at hand makes changes to the underlying repository while it's executing.
16-
1714
**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/repository_files.html)
1815

1916
## Example Usage

internal/provider/resource_gitlab_repository_file.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,23 @@ import (
1414

1515
const encoding = "base64"
1616

17+
// NOTE: this lock is a bit of a hack to prevent parallel calls to the GitLab Repository Files API.
18+
// If it is called concurrently, the API will return a 400 error along the lines of:
19+
// ```
20+
// (400 Bad Request) DELETE https://gitlab.com/api/v4/projects/30716/repository/files/somefile.yaml: 400
21+
// {message: 9:Could not update refs/heads/master. Please refresh and try again..}
22+
// ```
23+
//
24+
// This lock only solves half of the problem, where the provider is responsible for
25+
// the concurrency. The other half is if the API is called outside of terraform at the same time
26+
// this resource makes calls to the API.
27+
// To mitigate this, simple retries are used.
28+
var resourceGitlabRepositoryFileApiLock = newLock()
29+
1730
var _ = registerResource("gitlab_repository_file", func() *schema.Resource {
1831
return &schema.Resource{
1932
Description: `The ` + "`gitlab_repository_file`" + ` resource allows to manage the lifecycle of a file within a repository.
2033
21-
~> **Limitations**: The [GitLab Repository Files API](https://docs.gitlab.com/ee/api/repository_files.html) can only create, update or delete a single file at the time. The API will also [fail with a 400](https://docs.gitlab.com/ee/api/repository_files.html#update-existing-file-in-repository) response status code if the underlying repository is changed while the API tries to make changes. Therefore, it's recommended to make sure that you execute it with [-parallelism=1](https://www.terraform.io/docs/cli/commands/apply.html#parallelism-n) and that no other entity than the terraform at hand makes changes to the underlying repository while it's executing.
22-
2334
**Upstream API**: [GitLab REST API docs](https://docs.gitlab.com/ee/api/repository_files.html)`,
2435

2536
CreateContext: resourceGitlabRepositoryFileCreate,
@@ -69,10 +80,18 @@ var _ = registerResource("gitlab_repository_file", func() *schema.Resource {
6980
})
7081

7182
func resourceGitlabRepositoryFileCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
72-
client := meta.(*gitlab.Client)
7383
project := d.Get("project").(string)
7484
filePath := d.Get("file_path").(string)
7585

86+
log.Printf("[DEBUG] gitlab_repository_file: waiting for lock to create %s/%s", project, filePath)
87+
if err := resourceGitlabRepositoryFileApiLock.lock(ctx); err != nil {
88+
return diag.FromErr(err)
89+
}
90+
defer resourceGitlabRepositoryFileApiLock.unlock()
91+
log.Printf("[DEBUG] gitlab_repository_file: got lock to create %s/%s", project, filePath)
92+
93+
client := meta.(*gitlab.Client)
94+
7695
options := &gitlab.CreateFileOptions{
7796
Branch: gitlab.String(d.Get("branch").(string)),
7897
Encoding: gitlab.String(encoding),
@@ -126,12 +145,20 @@ func resourceGitlabRepositoryFileRead(ctx context.Context, d *schema.ResourceDat
126145
}
127146

128147
func resourceGitlabRepositoryFileUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
129-
client := meta.(*gitlab.Client)
130148
project, branch, filePath, err := resourceGitLabRepositoryFileParseId(d.Id())
131149
if err != nil {
132150
return diag.FromErr(err)
133151
}
134152

153+
log.Printf("[DEBUG] gitlab_repository_file: waiting for lock to update %s/%s", project, filePath)
154+
if err := resourceGitlabRepositoryFileApiLock.lock(ctx); err != nil {
155+
return diag.FromErr(err)
156+
}
157+
defer resourceGitlabRepositoryFileApiLock.unlock()
158+
log.Printf("[DEBUG] gitlab_repository_file: got lock to update %s/%s", project, filePath)
159+
160+
client := meta.(*gitlab.Client)
161+
135162
readOptions := &gitlab.GetFileOptions{
136163
Ref: gitlab.String(branch),
137164
}
@@ -163,12 +190,20 @@ func resourceGitlabRepositoryFileUpdate(ctx context.Context, d *schema.ResourceD
163190
}
164191

165192
func resourceGitlabRepositoryFileDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
166-
client := meta.(*gitlab.Client)
167193
project, branch, filePath, err := resourceGitLabRepositoryFileParseId(d.Id())
168194
if err != nil {
169195
return diag.FromErr(err)
170196
}
171197

198+
log.Printf("[DEBUG] gitlab_repository_file: waiting for lock to delete %s/%s", project, filePath)
199+
if err := resourceGitlabRepositoryFileApiLock.lock(ctx); err != nil {
200+
return diag.FromErr(err)
201+
}
202+
defer resourceGitlabRepositoryFileApiLock.unlock()
203+
log.Printf("[DEBUG] gitlab_repository_file: got lock to delete %s/%s", project, filePath)
204+
205+
client := meta.(*gitlab.Client)
206+
172207
readOptions := &gitlab.GetFileOptions{
173208
Ref: gitlab.String(branch),
174209
}

internal/provider/resource_gitlab_repository_file_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,32 @@ func TestAccGitlabRepositoryFile_createSameFileDifferentRepository(t *testing.T)
6262
})
6363
}
6464

65+
func TestAccGitlabRepositoryFile_concurrentResources(t *testing.T) {
66+
testAccCheck(t)
67+
68+
testProject := testAccCreateProject(t)
69+
70+
resource.Test(t, resource.TestCase{
71+
PreCheck: func() { testAccPreCheck(t) },
72+
ProviderFactories: providerFactories,
73+
CheckDestroy: testAccCheckGitlabRepositoryFileDestroy,
74+
Steps: []resource.TestStep{
75+
// NOTE: we don't need to check anything here, just make sure no terraform errors are being raised,
76+
// the other test cases will do the actual testing :)
77+
{
78+
Config: testAccGitlabRepositoryFileConcurrentResourcesConfig(testProject.ID),
79+
},
80+
{
81+
Config: testAccGitlabRepositoryFileConcurrentResourcesConfigUpdate(testProject.ID),
82+
},
83+
{
84+
Config: testAccGitlabRepositoryFileConcurrentResourcesConfigUpdate(testProject.ID),
85+
Destroy: true,
86+
},
87+
},
88+
})
89+
}
90+
6591
func TestAccGitlabRepositoryFile_validationOfBase64Content(t *testing.T) {
6692
cases := []struct {
6793
givenContent string
@@ -366,3 +392,31 @@ resource "gitlab_repository_file" "bar_file" {
366392
}
367393
`, rInt, rInt)
368394
}
395+
396+
func testAccGitlabRepositoryFileConcurrentResourcesConfig(projectID int) string {
397+
return fmt.Sprintf(`
398+
resource "gitlab_repository_file" "this" {
399+
project = "%d"
400+
file_path = "file-${count.index}.txt"
401+
branch = "main"
402+
content = base64encode("content-${count.index}")
403+
commit_message = "Add file ${count.index}"
404+
405+
count = 50
406+
}
407+
`, projectID)
408+
}
409+
410+
func testAccGitlabRepositoryFileConcurrentResourcesConfigUpdate(projectID int) string {
411+
return fmt.Sprintf(`
412+
resource "gitlab_repository_file" "this" {
413+
project = "%d"
414+
file_path = "file-${count.index}.txt"
415+
branch = "main"
416+
content = base64encode("updated-content-${count.index}")
417+
commit_message = "Add file ${count.index}"
418+
419+
count = 50
420+
}
421+
`, projectID)
422+
}

internal/provider/util.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package provider
22

33
import (
4+
"context"
45
"fmt"
56
"net/url"
67
"regexp"
@@ -400,3 +401,27 @@ func setStateMapInResourceData(stateMap map[string]interface{}, d *schema.Resour
400401

401402
return nil
402403
}
404+
405+
// lock can be used to lock, but make it `context.Context` aware.
406+
// e.g. it'll respect cancelling and timeouts.
407+
type lock chan struct{}
408+
409+
func newLock() lock {
410+
return make(lock, 1)
411+
412+
}
413+
414+
func (c lock) lock(ctx context.Context) error {
415+
select {
416+
case c <- struct{}{}:
417+
// lock acquired
418+
return nil
419+
case <-ctx.Done():
420+
// Timeout
421+
return ctx.Err()
422+
}
423+
}
424+
425+
func (c lock) unlock() {
426+
<-c
427+
}

0 commit comments

Comments
 (0)