diff --git a/.golangci.yml b/.golangci.yml index 17dec8e..00760b8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -60,7 +60,6 @@ linters: - "github.com/google/uuid$" - "github.com/DependencyTrack/client-go$" - "terraform-provider-dependencytrack/internal/provider$" - dogsled: max-blank-identifiers: 2 # Default 2 dupl: @@ -585,9 +584,11 @@ linters: - path: internal/provider/tag_projects_resource.go linters: - gocognit + - godox - path: internal/provider/tag_policies_resource.go linters: - gocognit + - godox - path: internal/provider/component_resource.go linters: - revive diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c6ca8..668a4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.17.2 + +#### FIXES +- `tags` in `dependencytrack_project` no longer needs to be sorted. + - https://github.com/SolarFactories/terraform-provider-dependencytrack/issues/152 + +#### DEPENDENCIES +- `actions/checkout` `6.0.1` -> `6.0.2` + ## 1.17.1 #### FIXES diff --git a/internal/provider/project_resource.go b/internal/provider/project_resource.go index 7f2d7c7..817bc9a 100644 --- a/internal/provider/project_resource.go +++ b/internal/provider/project_resource.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "strings" dtrack "github.com/DependencyTrack/client-go" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -174,7 +175,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest } } if !plan.Tags.IsUnknown() && !plan.Tags.IsNull() { - strings, err := GetStringList(ctx, &resp.Diagnostics, plan.Tags) + tagStrings, err := GetStringList(ctx, &resp.Diagnostics, plan.Tags) if resp.Diagnostics.HasError() { return } @@ -186,7 +187,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest ) return } - projectReq.Tags = Map(strings, func(item string) dtrack.Tag { return dtrack.Tag{Name: item} }) + projectReq.Tags = Map(tagStrings, func(item string) dtrack.Tag { return dtrack.Tag{Name: item} }) } if plan.Active.IsUnknown() { projectReq.Active = true @@ -228,7 +229,13 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest ) return } - tagValueList := Map(projectRes.Tags, func(tag dtrack.Tag) attr.Value { + resTags := projectRes.Tags + if SliceUnorderedEqual(projectReq.Tags, projectRes.Tags, func(req, res dtrack.Tag) int { + return strings.Compare(req.Name, res.Name) + }) { + resTags = projectReq.Tags + } + tagValueList := Map(resTags, func(tag dtrack.Tag) attr.Value { return types.StringValue(tag.Name) }) tagList, diags := types.ListValue(types.StringType, tagValueList) @@ -337,9 +344,20 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re return } - tagValueList := Map(project.Tags, func(tag dtrack.Tag) attr.Value { - return types.StringValue(tag.Name) - }) + stateTags, err := GetStringList(ctx, &resp.Diagnostics, state.Tags) + if err != nil { + resp.Diagnostics.AddError( + "Unable to load current tags on project", + "Error with transforming stored tags on project: "+id.String()+", in original error: "+err.Error(), + ) + return + } + returnedTags := Map(project.Tags, func(tag dtrack.Tag) string { return tag.Name }) + newStateTags := returnedTags + if SliceUnorderedEqual(stateTags, returnedTags, strings.Compare) { + newStateTags = stateTags + } + tagValueList := Map(newStateTags, func(name string) attr.Value { return types.StringValue(name) }) tagList, diags := types.ListValue(types.StringType, tagValueList) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/internal/provider/project_resource_test.go b/internal/provider/project_resource_test.go index 4fc9b7d..f199235 100644 --- a/internal/provider/project_resource_test.go +++ b/internal/provider/project_resource_test.go @@ -405,3 +405,48 @@ resource "dependencytrack_project" "test3" { }, }) } + +func TestAccProjectResourceRegression152(t *testing.T) { + // Regression test for https://github.com/SolarFactories/terraform-provider-dependencytrack/issues/152 + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing. + { + Config: providerConfig + ` +resource "dependencytrack_project" "example" { + name = "example-project" + description = "Example project" + tags = [ + "collection-example", + "environment-test" + ] +} +`, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.#", "2"), + resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.0", "collection-example"), + resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.1", "environment-test"), + ), + }, + // Update and Read testing. + { + Config: providerConfig + ` +resource "dependencytrack_project" "example" { + name = "example-project" + description = "Example project With Change" + tags = [ + "collection-example", + "environment-test" + ] +} +`, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.#", "2"), + resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.0", "collection-example"), + resource.TestCheckResourceAttr("dependencytrack_project.example", "tags.1", "environment-test"), + ), + }, + }, + }) +} diff --git a/internal/provider/tag_policies_resource.go b/internal/provider/tag_policies_resource.go index ff3993f..c2b1f4a 100644 --- a/internal/provider/tag_policies_resource.go +++ b/internal/provider/tag_policies_resource.go @@ -61,6 +61,7 @@ func (*tagPoliciesResource) Schema(_ context.Context, _ resource.SchemaRequest, }, }, "policies": schema.ListAttribute{ + // TODO: Use `ListUnorderedEqual` to remove requirement for this to be sorted. Description: "Policy UUIDs to which to apply tag. Sorted by policy name.", Required: true, ElementType: types.StringType, diff --git a/internal/provider/tag_projects_resource.go b/internal/provider/tag_projects_resource.go index b3ce258..9186c6d 100644 --- a/internal/provider/tag_projects_resource.go +++ b/internal/provider/tag_projects_resource.go @@ -61,6 +61,7 @@ func (*tagProjectsResource) Schema(_ context.Context, _ resource.SchemaRequest, }, }, "projects": schema.ListAttribute{ + // TODO: Use `ListUnorderedEqual` to remove requirement for this to be sorted. Description: "Project UUIDs to which to apply tag. Sorted by project name.", Required: true, ElementType: types.StringType, diff --git a/internal/provider/util.go b/internal/provider/util.go index 27b4560..8f922fd 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -279,3 +279,9 @@ func GetStringList(ctx context.Context, diags *diag.Diagnostics, list types.List }) return stringList, err } + +func SliceUnorderedEqual[T any](a, b []T, compare func(a, b T) int) bool { + sortedA := slices.SortedStableFunc(slices.Values(a), compare) + sortedB := slices.SortedStableFunc(slices.Values(b), compare) + return slices.EqualFunc(sortedA, sortedB, func(a, b T) bool { return compare(a, b) == 0 }) +} diff --git a/internal/provider/util_test.go b/internal/provider/util_test.go index 68718ca..039c9ba 100644 --- a/internal/provider/util_test.go +++ b/internal/provider/util_test.go @@ -3,6 +3,7 @@ package provider import ( "cmp" "regexp" + "strings" "testing" ) @@ -63,6 +64,33 @@ func TestParseSemver(t *testing.T) { } } +func TestSliceUnorderedEqual(t *testing.T) { + { + a := []int{1, 20, 15, 18} + b := []int{1, 15, 18, 20} + equal := SliceUnorderedEqual(a, b, func(a, b int) int { return b - a }) + requireEqual(t, equal, true) + } + { + a := []int{1, 20, 15, 19} + b := []int{1, 15, 18, 20} + equal := SliceUnorderedEqual(a, b, func(a, b int) int { return b - a }) + requireEqual(t, equal, false) + } + { + a := []int{} + b := []int{} + equal := SliceUnorderedEqual(a, b, func(a, b int) int { return b - a }) + requireEqual(t, equal, true) + } + { + a := []string{"Foo", "Bar"} + b := []string{"Bar", "Foo"} + equal := SliceUnorderedEqual(a, b, strings.Compare) + requireEqual(t, equal, true) + } +} + func requireNoError(t *testing.T, actual error) { t.Helper() if actual != nil { @@ -93,7 +121,7 @@ func requireNil[T any](t *testing.T, actual *T) { } } -func requireEqual[T cmp.Ordered](t *testing.T, actual T, expected T) { +func requireEqual[T cmp.Ordered | bool](t *testing.T, actual T, expected T) { t.Helper() if actual != expected { t.Errorf("Expected %v, received %v", expected, actual)