Skip to content

Commit 1ada480

Browse files
Merge pull request #211 from jfrog/GH-209-share-repo
GH-209 Fixing race condition in project_share_repository resource, ad…
2 parents 162a266 + c730ade commit 1ada480

File tree

9 files changed

+272
-16
lines changed

9 files changed

+272
-16
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 1.9.4. (August 22, 2025). Tested on Artifactory 7.117.14 with Terraform 1.13.0 and OpenTofu 1.10.5
2+
3+
BUG FIXES:
4+
5+
* resource/project_share_repository: Fixed race condition, when the repository couldn't be shared with multiple projects in a loop. Mutex was added tomitigate the overwhelming of the endpoint. Issue: [#209](https://github.com/jfrog/terraform-provider-project/issues/209) PR: [#211](https://github.com/jfrog/terraform-provider-project/pull/211)
6+
*
7+
18
## 1.9.3 (December 19, 2024). Tested on Artifactory 7.98.11 with Terraform 1.10.3 and OpenTofu 1.8.7
29

310
BUG FIXES:

docs/resources/share_repository.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,27 @@ Share a local or remote repository with a list of projects. Project Members of t
1616
## Example Usage
1717

1818
```terraform
19+
# Share repository with a project
1920
resource "project_share_repository" "myprojectsharerepo" {
2021
repo_key = "myrepo-generic-local"
2122
target_project_key = "myproj"
2223
}
24+
25+
# Share repository with multiple projects
26+
resource "project_share_repository" "share_repo" {
27+
count = 3
28+
29+
repo_key = artifactory_local_generic_repository.repo.key
30+
target_project_key = element(
31+
[
32+
project.project_name_1.key,
33+
project.project_name_2.key,
34+
project.project_name_3.key
35+
],
36+
count.index
37+
)
38+
read_only = true
39+
}
2340
```
2441

2542
<!-- schema generated by tfplugindocs -->
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
1+
# Share repository with a project
12
resource "project_share_repository" "myprojectsharerepo" {
23
repo_key = "myrepo-generic-local"
34
target_project_key = "myproj"
4-
}
5+
}
6+
7+
# Share repository with multiple projects
8+
resource "project_share_repository" "share_repo" {
9+
count = 3
10+
11+
repo_key = artifactory_local_generic_repository.repo.key
12+
target_project_key = element(
13+
[
14+
project.project_name_1.key,
15+
project.project_name_2.key,
16+
project.project_name_3.key
17+
],
18+
count.index
19+
)
20+
read_only = true
21+
}

pkg/project/resource/repo_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestAccProject_repo(t *testing.T) {
3131
key = "{{ .repo1 }}"
3232
3333
lifecycle {
34-
ignore_changes = [project_key]
34+
ignore_changes = [project_key, project_environments]
3535
}
3636
}
3737
@@ -56,15 +56,15 @@ func TestAccProject_repo(t *testing.T) {
5656
key = "{{ .repo1 }}"
5757
5858
lifecycle {
59-
ignore_changes = [project_key]
59+
ignore_changes = ["project_key", "project_environments"]
6060
}
6161
}
6262
6363
resource "artifactory_local_generic_repository" "{{ .repo2 }}" {
6464
key = "{{ .repo2 }}"
6565
6666
lifecycle {
67-
ignore_changes = [project_key]
67+
ignore_changes = ["project_key", "project_environments"]
6868
}
6969
}
7070
@@ -201,7 +201,7 @@ func TestAccProject_repoAssignMultipleRepos(t *testing.T) {
201201
key = "{{ $repoName }}"
202202
203203
lifecycle {
204-
ignore_changes = [project_key]
204+
ignore_changes = [project_key, project_environments]
205205
}
206206
}
207207
{{ end }}
@@ -228,7 +228,7 @@ func TestAccProject_repoAssignMultipleRepos(t *testing.T) {
228228
key = "{{ $repoName }}"
229229
230230
lifecycle {
231-
ignore_changes = [project_key]
231+
ignore_changes = ["project_key", "project_environments"]
232232
}
233233
}
234234
{{ end }}
@@ -317,7 +317,7 @@ func TestAccProject_repoUnassignNonexistantRepo(t *testing.T) {
317317
key = "{{ .repo }}"
318318
319319
lifecycle {
320-
ignore_changes = [project_key]
320+
ignore_changes = ["project_key", "project_environments"]
321321
}
322322
}
323323

pkg/project/resource/resource_project_share_repository.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ func (r *ProjectShareRepositoryResource) Configure(ctx context.Context, req reso
112112
func (r *ProjectShareRepositoryResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
113113
go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName)
114114

115+
lockName := "share"
116+
GlobalMutex.Lock(lockName)
117+
defer GlobalMutex.Unlock(lockName)
118+
115119
var plan ProjectShareRepositoryResourceModel
116120

117121
// Read Terraform plan data into the model
@@ -204,6 +208,10 @@ func (r *ProjectShareRepositoryResource) Update(ctx context.Context, req resourc
204208
func (r *ProjectShareRepositoryResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
205209
go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName)
206210

211+
lockName := "share"
212+
GlobalMutex.Lock(lockName)
213+
defer GlobalMutex.Unlock(lockName)
214+
207215
var state ProjectShareRepositoryResourceModel
208216

209217
// Read Terraform prior state data into the model

pkg/project/resource/resource_project_share_repository_test.go

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
)
1313

1414
func TestAccProjectShareRepository_full(t *testing.T) {
15-
t.Skip("project API is not returning/setting read_only field correctly")
1615

1716
client := acctest.GetTestResty(t)
1817
version, err := util.GetArtifactoryVersion(client)
@@ -48,7 +47,7 @@ func TestAccProjectShareRepository_full(t *testing.T) {
4847
key = "{{ .repo_key }}"
4948
5049
lifecycle {
51-
ignore_changes = ["project_key"]
50+
ignore_changes = ["project_key", "project_environments"]
5251
}
5352
}
5453
@@ -80,7 +79,7 @@ func TestAccProjectShareRepository_full(t *testing.T) {
8079
key = "{{ .repo_key }}"
8180
8281
lifecycle {
83-
ignore_changes = ["project_key"]
82+
ignore_changes = ["project_key", "project_environments"]
8483
}
8584
}
8685
@@ -147,3 +146,170 @@ func TestAccProjectShareRepository_full(t *testing.T) {
147146
},
148147
})
149148
}
149+
150+
func TestAccProjectShareRepositoryWithMultipleProjects(t *testing.T) {
151+
// The goal of this test is to simulate the race condition, when the repository was only shared with one project (first in the list) if
152+
// the loop was used in "project_share_repository" resource
153+
client := acctest.GetTestResty(t)
154+
version, err := util.GetArtifactoryVersion(client)
155+
if err != nil {
156+
t.Fatal(err)
157+
}
158+
valid, err := util.CheckVersion(version, "7.90.1")
159+
if err != nil {
160+
t.Fatal(err)
161+
}
162+
if !valid {
163+
t.Skipf("Artifactory version %s is earlier than 7.90.1", version)
164+
}
165+
166+
projectKey1 := strings.ToLower(acctest.RandSeq(10))
167+
projectKey2 := strings.ToLower(acctest.RandSeq(10))
168+
projectKey3 := strings.ToLower(acctest.RandSeq(10))
169+
projectKey4 := strings.ToLower(acctest.RandSeq(10))
170+
projectName1 := fmt.Sprintf("tftestprojects1_%s", projectKey1)
171+
projectName2 := fmt.Sprintf("tftestprojects2_%s", projectKey2)
172+
projectName3 := fmt.Sprintf("tftestprojects3_%s", projectKey3)
173+
projectName4 := fmt.Sprintf("tftestprojects4_%s", projectKey4)
174+
175+
repoKey := fmt.Sprintf("repo%d", testutil.RandomInt())
176+
177+
fqrn := "project_share_repository.share_repo"
178+
179+
params := map[string]string{
180+
"project_name_1": projectName1,
181+
"project_key_1": projectKey1,
182+
"project_name_2": projectName2,
183+
"project_key_2": projectKey2,
184+
"project_name_3": projectName3,
185+
"project_key_3": projectKey3,
186+
"project_name_4": projectName4,
187+
"project_key_4": projectKey4,
188+
"repo_key": repoKey,
189+
}
190+
// Creating projects without for each loop, they are not supported in tf tests
191+
temp := `
192+
resource "artifactory_local_generic_repository" "repo" {
193+
key = "{{ .repo_key }}"
194+
description = "Lab repository for troubleshooting - {{ .repo_key }}"
195+
196+
lifecycle {
197+
ignore_changes = ["project_key", "project_environments"]
198+
}
199+
}
200+
201+
# Create 4 projects
202+
resource "project" "{{ .project_name_1 }}" {
203+
key = "{{ .project_key_1 }}"
204+
display_name = "{{ .project_name_1 }}"
205+
description = "test description"
206+
admin_privileges {
207+
manage_members = true
208+
manage_resources = true
209+
index_resources = true
210+
}
211+
max_storage_in_gibibytes = 1
212+
block_deployments_on_limit = true
213+
email_notification = false
214+
}
215+
216+
resource "project" "{{ .project_name_2 }}" {
217+
key = "{{ .project_key_2 }}"
218+
display_name = "{{ .project_name_2 }}"
219+
description = "test description"
220+
admin_privileges {
221+
manage_members = true
222+
manage_resources = true
223+
index_resources = true
224+
}
225+
max_storage_in_gibibytes = 1
226+
block_deployments_on_limit = true
227+
email_notification = false
228+
}
229+
230+
resource "project" "{{ .project_name_3 }}" {
231+
key = "{{ .project_key_3 }}"
232+
display_name = "{{ .project_name_3 }}"
233+
description = "test description"
234+
admin_privileges {
235+
manage_members = true
236+
manage_resources = true
237+
index_resources = true
238+
}
239+
max_storage_in_gibibytes = 1
240+
block_deployments_on_limit = true
241+
email_notification = false
242+
}
243+
244+
resource "project" "{{ .project_name_4 }}" {
245+
key = "{{ .project_key_4 }}"
246+
display_name = "{{ .project_name_4 }}"
247+
description = "test description"
248+
admin_privileges {
249+
manage_members = true
250+
manage_resources = true
251+
index_resources = true
252+
}
253+
max_storage_in_gibibytes = 1
254+
block_deployments_on_limit = true
255+
email_notification = false
256+
}
257+
258+
# Add repository to {{ .project_name_1 }}
259+
resource "project_repository" "add_to_project1" {
260+
project_key = project.{{ .project_name_1 }}.key
261+
key = artifactory_local_generic_repository.repo.key
262+
}
263+
264+
# Share ONE repo with the other three projects
265+
resource "project_share_repository" "share_repo" {
266+
count = 3
267+
268+
repo_key = artifactory_local_generic_repository.repo.key
269+
target_project_key = element(
270+
[
271+
project.{{ .project_name_2 }}.key,
272+
project.{{ .project_name_3 }}.key,
273+
project.{{ .project_name_4 }}.key
274+
],
275+
count.index
276+
)
277+
read_only = true
278+
depends_on = [project_repository.add_to_project1]
279+
}
280+
`
281+
282+
config := util.ExecuteTemplate("TestAccProjectShareRepository", temp, params)
283+
284+
resource.Test(t, resource.TestCase{
285+
PreCheck: func() { acctest.PreCheck(t) },
286+
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
287+
ExternalProviders: map[string]resource.ExternalProvider{
288+
"artifactory": {
289+
Source: "jfrog/artifactory",
290+
},
291+
},
292+
Steps: []resource.TestStep{
293+
{
294+
Config: config,
295+
Check: resource.ComposeTestCheckFunc(
296+
resource.TestCheckResourceAttr(fmt.Sprintf("%s.0", fqrn), "repo_key", params["repo_key"]),
297+
resource.TestCheckResourceAttr(fmt.Sprintf("%s.0", fqrn), "target_project_key", params["project_key_2"]),
298+
resource.TestCheckResourceAttr(fmt.Sprintf("%s.0", fqrn), "read_only", "true"),
299+
resource.TestCheckResourceAttr(fmt.Sprintf("%s.1", fqrn), "repo_key", params["repo_key"]),
300+
resource.TestCheckResourceAttr(fmt.Sprintf("%s.1", fqrn), "target_project_key", params["project_key_3"]),
301+
resource.TestCheckResourceAttr(fmt.Sprintf("%s.1", fqrn), "read_only", "true"),
302+
resource.TestCheckResourceAttr(fmt.Sprintf("%s.2", fqrn), "repo_key", params["repo_key"]),
303+
resource.TestCheckResourceAttr(fmt.Sprintf("%s.2", fqrn), "target_project_key", params["project_key_4"]),
304+
resource.TestCheckResourceAttr(fmt.Sprintf("%s.2", fqrn), "read_only", "true"),
305+
),
306+
},
307+
{
308+
ResourceName: fmt.Sprintf("%s[0]", fqrn),
309+
ImportStateId: fmt.Sprintf("%s:%s", params["repo_key"], params["project_key_2"]),
310+
ImportState: true,
311+
ImportStateVerify: false,
312+
},
313+
},
314+
})
315+
}

pkg/project/resource/resource_project_share_repository_with_all_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func TestAccProjectShareWithAllRepository_full(t *testing.T) {
4545
key = "{{ .repo_key }}"
4646
4747
lifecycle {
48-
ignore_changes = ["project_key"]
48+
ignore_changes = ["project_key", "project_environments"]
4949
}
5050
}
5151
@@ -80,7 +80,7 @@ func TestAccProjectShareWithAllRepository_full(t *testing.T) {
8080
key = "{{ .repo_key }}"
8181
8282
lifecycle {
83-
ignore_changes = ["project_key"]
83+
ignore_changes = ["project_key", "project_environments"]
8484
}
8585
}
8686

pkg/project/resource/resource_project_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,15 @@ func TestAccProject_UpgradeFromSDKv2(t *testing.T) {
7474
key = "{{ .repo1 }}"
7575
7676
lifecycle {
77-
ignore_changes = ["project_key"]
77+
ignore_changes = ["project_key", "project_environments"]
7878
}
7979
}
8080
8181
resource "artifactory_local_generic_repository" "{{ .repo2 }}" {
8282
key = "{{ .repo2 }}"
8383
8484
lifecycle {
85-
ignore_changes = ["project_key"]
85+
ignore_changes = ["project_key", "project_environments"]
8686
}
8787
}
8888
@@ -506,15 +506,15 @@ func TestAccProject_full(t *testing.T) {
506506
key = "{{ .repo1 }}"
507507
508508
lifecycle {
509-
ignore_changes = ["project_key"]
509+
ignore_changes = [project_key, project_environments]
510510
}
511511
}
512512
513513
resource "artifactory_local_generic_repository" "{{ .repo2 }}" {
514514
key = "{{ .repo2 }}"
515515
516516
lifecycle {
517-
ignore_changes = ["project_key"]
517+
ignore_changes = [project_key, project_environments]
518518
}
519519
}
520520

0 commit comments

Comments
 (0)