Skip to content

Commit 5a6b7c7

Browse files
BugFix 152: Project Tag Ordering / Release v1.17.2 (#154)
* Added utility function SliceUnorderedEqual. * Add regression test for issue of DT API sorting tags within Project. * Update Create logic for dependencytrack_project. * Update Read logic for dependencytrack_project. * Identified other locations of sorted unordered lists, to be resolved separately. * Update CHANGELOG for release.
1 parent 8c3274c commit 5a6b7c7

File tree

8 files changed

+117
-8
lines changed

8 files changed

+117
-8
lines changed

.golangci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ linters:
6060
- "github.com/google/uuid$"
6161
- "github.com/DependencyTrack/client-go$"
6262
- "terraform-provider-dependencytrack/internal/provider$"
63-
6463
dogsled:
6564
max-blank-identifiers: 2 # Default 2
6665
dupl:
@@ -585,9 +584,11 @@ linters:
585584
- path: internal/provider/tag_projects_resource.go
586585
linters:
587586
- gocognit
587+
- godox
588588
- path: internal/provider/tag_policies_resource.go
589589
linters:
590590
- gocognit
591+
- godox
591592
- path: internal/provider/component_resource.go
592593
linters:
593594
- revive

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 1.17.2
2+
3+
#### FIXES
4+
- `tags` in `dependencytrack_project` no longer needs to be sorted.
5+
- https://github.com/SolarFactories/terraform-provider-dependencytrack/issues/152
6+
7+
#### DEPENDENCIES
8+
- `actions/checkout` `6.0.1` -> `6.0.2`
9+
110
## 1.17.1
211

312
#### FIXES

internal/provider/project_resource.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package provider
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
dtrack "github.com/DependencyTrack/client-go"
89
"github.com/hashicorp/terraform-plugin-framework/attr"
@@ -174,7 +175,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest
174175
}
175176
}
176177
if !plan.Tags.IsUnknown() && !plan.Tags.IsNull() {
177-
strings, err := GetStringList(ctx, &resp.Diagnostics, plan.Tags)
178+
tagStrings, err := GetStringList(ctx, &resp.Diagnostics, plan.Tags)
178179
if resp.Diagnostics.HasError() {
179180
return
180181
}
@@ -186,7 +187,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest
186187
)
187188
return
188189
}
189-
projectReq.Tags = Map(strings, func(item string) dtrack.Tag { return dtrack.Tag{Name: item} })
190+
projectReq.Tags = Map(tagStrings, func(item string) dtrack.Tag { return dtrack.Tag{Name: item} })
190191
}
191192
if plan.Active.IsUnknown() {
192193
projectReq.Active = true
@@ -228,7 +229,13 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest
228229
)
229230
return
230231
}
231-
tagValueList := Map(projectRes.Tags, func(tag dtrack.Tag) attr.Value {
232+
resTags := projectRes.Tags
233+
if SliceUnorderedEqual(projectReq.Tags, projectRes.Tags, func(req, res dtrack.Tag) int {
234+
return strings.Compare(req.Name, res.Name)
235+
}) {
236+
resTags = projectReq.Tags
237+
}
238+
tagValueList := Map(resTags, func(tag dtrack.Tag) attr.Value {
232239
return types.StringValue(tag.Name)
233240
})
234241
tagList, diags := types.ListValue(types.StringType, tagValueList)
@@ -337,9 +344,20 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re
337344
return
338345
}
339346

340-
tagValueList := Map(project.Tags, func(tag dtrack.Tag) attr.Value {
341-
return types.StringValue(tag.Name)
342-
})
347+
stateTags, err := GetStringList(ctx, &resp.Diagnostics, state.Tags)
348+
if err != nil {
349+
resp.Diagnostics.AddError(
350+
"Unable to load current tags on project",
351+
"Error with transforming stored tags on project: "+id.String()+", in original error: "+err.Error(),
352+
)
353+
return
354+
}
355+
returnedTags := Map(project.Tags, func(tag dtrack.Tag) string { return tag.Name })
356+
newStateTags := returnedTags
357+
if SliceUnorderedEqual(stateTags, returnedTags, strings.Compare) {
358+
newStateTags = stateTags
359+
}
360+
tagValueList := Map(newStateTags, func(name string) attr.Value { return types.StringValue(name) })
343361
tagList, diags := types.ListValue(types.StringType, tagValueList)
344362
resp.Diagnostics.Append(diags...)
345363
if resp.Diagnostics.HasError() {

internal/provider/project_resource_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,48 @@ resource "dependencytrack_project" "test3" {
405405
},
406406
})
407407
}
408+
409+
func TestAccProjectResourceRegression152(t *testing.T) {
410+
// Regression test for https://github.com/SolarFactories/terraform-provider-dependencytrack/issues/152
411+
resource.Test(t, resource.TestCase{
412+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
413+
Steps: []resource.TestStep{
414+
// Create and Read testing.
415+
{
416+
Config: providerConfig + `
417+
resource "dependencytrack_project" "example" {
418+
name = "example-project"
419+
description = "Example project"
420+
tags = [
421+
"collection-example",
422+
"environment-test"
423+
]
424+
}
425+
`,
426+
Check: resource.ComposeAggregateTestCheckFunc(
427+
resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.#", "2"),
428+
resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.0", "collection-example"),
429+
resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.1", "environment-test"),
430+
),
431+
},
432+
// Update and Read testing.
433+
{
434+
Config: providerConfig + `
435+
resource "dependencytrack_project" "example" {
436+
name = "example-project"
437+
description = "Example project With Change"
438+
tags = [
439+
"collection-example",
440+
"environment-test"
441+
]
442+
}
443+
`,
444+
Check: resource.ComposeAggregateTestCheckFunc(
445+
resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.#", "2"),
446+
resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.0", "collection-example"),
447+
resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.1", "environment-test"),
448+
),
449+
},
450+
},
451+
})
452+
}

internal/provider/tag_policies_resource.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func (*tagPoliciesResource) Schema(_ context.Context, _ resource.SchemaRequest,
6161
},
6262
},
6363
"policies": schema.ListAttribute{
64+
// TODO: Use `ListUnorderedEqual` to remove requirement for this to be sorted.
6465
Description: "Policy UUIDs to which to apply tag. Sorted by policy name.",
6566
Required: true,
6667
ElementType: types.StringType,

internal/provider/tag_projects_resource.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func (*tagProjectsResource) Schema(_ context.Context, _ resource.SchemaRequest,
6161
},
6262
},
6363
"projects": schema.ListAttribute{
64+
// TODO: Use `ListUnorderedEqual` to remove requirement for this to be sorted.
6465
Description: "Project UUIDs to which to apply tag. Sorted by project name.",
6566
Required: true,
6667
ElementType: types.StringType,

internal/provider/util.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,9 @@ func GetStringList(ctx context.Context, diags *diag.Diagnostics, list types.List
279279
})
280280
return stringList, err
281281
}
282+
283+
func SliceUnorderedEqual[T any](a, b []T, compare func(a, b T) int) bool {
284+
sortedA := slices.SortedStableFunc(slices.Values(a), compare)
285+
sortedB := slices.SortedStableFunc(slices.Values(b), compare)
286+
return slices.EqualFunc(sortedA, sortedB, func(a, b T) bool { return compare(a, b) == 0 })
287+
}

internal/provider/util_test.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package provider
33
import (
44
"cmp"
55
"regexp"
6+
"strings"
67
"testing"
78
)
89

@@ -63,6 +64,33 @@ func TestParseSemver(t *testing.T) {
6364
}
6465
}
6566

67+
func TestSliceUnorderedEqual(t *testing.T) {
68+
{
69+
a := []int{1, 20, 15, 18}
70+
b := []int{1, 15, 18, 20}
71+
equal := SliceUnorderedEqual(a, b, func(a, b int) int { return b - a })
72+
requireEqual(t, equal, true)
73+
}
74+
{
75+
a := []int{1, 20, 15, 19}
76+
b := []int{1, 15, 18, 20}
77+
equal := SliceUnorderedEqual(a, b, func(a, b int) int { return b - a })
78+
requireEqual(t, equal, false)
79+
}
80+
{
81+
a := []int{}
82+
b := []int{}
83+
equal := SliceUnorderedEqual(a, b, func(a, b int) int { return b - a })
84+
requireEqual(t, equal, true)
85+
}
86+
{
87+
a := []string{"Foo", "Bar"}
88+
b := []string{"Bar", "Foo"}
89+
equal := SliceUnorderedEqual(a, b, strings.Compare)
90+
requireEqual(t, equal, true)
91+
}
92+
}
93+
6694
func requireNoError(t *testing.T, actual error) {
6795
t.Helper()
6896
if actual != nil {
@@ -93,7 +121,7 @@ func requireNil[T any](t *testing.T, actual *T) {
93121
}
94122
}
95123

96-
func requireEqual[T cmp.Ordered](t *testing.T, actual T, expected T) {
124+
func requireEqual[T cmp.Ordered | bool](t *testing.T, actual T, expected T) {
97125
t.Helper()
98126
if actual != expected {
99127
t.Errorf("Expected %v, received %v", expected, actual)

0 commit comments

Comments
 (0)