Skip to content

Commit 3befa22

Browse files
authored
Sort permission lists as the last state (#247)
* When refreshing permission lists, use the ordering of values from the previous state for sorting values from the server * Upgrade to go 1.24
1 parent 4016746 commit 3befa22

File tree

8 files changed

+206
-77
lines changed

8 files changed

+206
-77
lines changed

.github/workflows/ci.yml

+5-11
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ on:
66
- main
77

88
env:
9-
GO_VERSION: "1.22.0"
109
FORCE_COLOR: 1
1110

1211
jobs:
@@ -35,17 +34,14 @@ jobs:
3534
outputs:
3635
test_files: ${{steps.compile-tests.outputs.test_files}}
3736
test_files_include: ${{steps.compile-tests.outputs.test_files_include}}
37+
go-version: ${{steps.setup-go.outputs.go-version}}
3838
steps:
3939
- uses: actions/checkout@v4
4040
- uses: actions/setup-go@v5
41+
id: setup-go
4142
with:
42-
go-version: ${{ env.GO_VERSION }}
43-
- uses: actions/cache@v4
44-
with:
45-
path: ~/go/pkg/mod
46-
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
47-
restore-keys: |
48-
${{ runner.os }}-go-
43+
go-version-file: 'go.mod'
44+
cache: 'true'
4945
- name: Compile tests
5046
id: compile-tests
5147
shell: python
@@ -100,7 +96,7 @@ jobs:
10096
terraform_wrapper: false
10197
- uses: actions/setup-go@v5
10298
with:
103-
go-version: ${{ env.GO_VERSION }}
99+
go-version: ${{needs.acctest-build.outputs.go-version}}
104100
cache: 'false'
105101
- name: Install gotestsum
106102
run: go install gotest.tools/gotestsum@latest
@@ -151,8 +147,6 @@ jobs:
151147
with:
152148
report_paths: './test-results/*.xml'
153149
include_passed: 'true'
154-
require_tests: 'true'
155-
fail_on_failure: 'true'
156150
comment: 'true'
157151
check_retries: 'true'
158152
flaky_summary: 'true'

.github/workflows/docs-check.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- name: Set up Go
1414
uses: actions/setup-go@v5
1515
with:
16-
go-version: '1.22'
16+
go-version: '1.24'
1717
cache: true
1818

1919
- name: Install tfplugindocs

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Port is the Developer Platform meant to supercharge your DevOps and Developers,
2222

2323
## Requirements
2424
- [Terraform](https://www.terraform.io/downloads.html)
25-
- [Go](https://golang.org/doc/install) >= 1.22 (to build the provider plugin)
25+
- [Go](https://golang.org/doc/install) >= 1.24 (to build the provider plugin)
2626
- [Port Credentials](https://docs.getport.io/build-your-software-catalog/sync-data-to-catalog/api/#find-your-port-credentials)
2727

2828
## Installation

go.mod

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
module github.com/port-labs/terraform-provider-port-labs/v2
22

3-
go 1.22
4-
5-
toolchain go1.22.0
3+
go 1.24
64

75
require (
86
github.com/go-resty/resty/v2 v2.13.1

internal/utils/slices.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package utils
2+
3+
import (
4+
"cmp"
5+
"iter"
6+
"maps"
7+
"slices"
8+
"strings"
9+
)
10+
11+
// SortSliceByOther sorts `slice` by the positioning of equal items that exist in the `other` slice.
12+
//
13+
// You can also provide `fallbackCompare` to be used if both items don't exist in the `other` slice. If only a single
14+
// item is missing from the `other` slice, it will do a simple compare (a > b / a < b / a == b).
15+
func SortSliceByOther[Slice ~[]E, E cmp.Ordered](slice, other Slice, fallbackCompare func(a, b E) int) Slice {
16+
positionsMap := maps.Collect(AllCrossed(other))
17+
return slices.SortedFunc(slices.Values(slice), CompareByMap(positionsMap, fallbackCompare))
18+
}
19+
20+
func SortStringSliceByOther(slice, other []string) []string {
21+
return SortSliceByOther(slice, other, strings.Compare)
22+
}
23+
24+
func Map[S, R any](items []S, fn func(S) R) []R {
25+
result := make([]R, 0, len(items))
26+
for _, item := range items {
27+
result = append(result, fn(item))
28+
}
29+
return result
30+
}
31+
32+
func AllCrossed[Slice ~[]E, E comparable](s Slice) iter.Seq2[E, int] {
33+
return func(yield func(E, int) bool) {
34+
for i, v := range s {
35+
if !yield(v, i) {
36+
return
37+
}
38+
}
39+
}
40+
}
41+
42+
func SimpleCompare[E cmp.Ordered](a, b E) int {
43+
switch true {
44+
case a < b:
45+
return -1
46+
case a > b:
47+
return 1
48+
default:
49+
return 0
50+
}
51+
}
52+
53+
func CompareByMap[E cmp.Ordered](m map[E]int, fallbackCompare func(a, b E) int) func(a, b E) int {
54+
return func(a, b E) int {
55+
aPos, okA := m[a]
56+
bPos, okB := m[b]
57+
if !okA && !okB {
58+
if fallbackCompare == nil {
59+
return SimpleCompare(a, b)
60+
}
61+
return fallbackCompare(a, b)
62+
}
63+
return aPos - bPos
64+
}
65+
}

port/action-permissions/refreshActionPermissionsToState.go

+18-26
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,23 @@ import (
88
)
99

1010
func (r *ActionPermissionsResource) refreshActionPermissionsState(state *ActionPermissionsModel, a *cli.ActionPermissions, actionId string) error {
11+
oldPermissions := state.Permissions
12+
1113
state.ID = types.StringValue(actionId)
1214
state.ActionIdentifier = types.StringValue(actionId)
1315
state.BlueprintIdentifier = types.StringNull()
1416
state.Permissions = &PermissionsModel{}
1517

1618
state.Permissions.Execute = &ExecuteModel{}
1719

18-
state.Permissions.Execute.Users = make([]types.String, len(a.Execute.Users))
19-
for i, u := range a.Execute.Users {
20-
state.Permissions.Execute.Users[i] = types.StringValue(u)
21-
}
22-
23-
state.Permissions.Execute.Roles = make([]types.String, len(a.Execute.Roles))
24-
for i, u := range a.Execute.Roles {
25-
state.Permissions.Execute.Roles[i] = types.StringValue(u)
26-
}
27-
28-
state.Permissions.Execute.Teams = make([]types.String, len(a.Execute.Teams))
29-
for i, u := range a.Execute.Teams {
30-
state.Permissions.Execute.Teams[i] = types.StringValue(u)
20+
if oldPermissions == nil || oldPermissions.Execute == nil {
21+
state.Permissions.Execute.Users = utils.Map(a.Execute.Users, types.StringValue)
22+
state.Permissions.Execute.Roles = utils.Map(a.Execute.Roles, types.StringValue)
23+
state.Permissions.Execute.Teams = utils.Map(a.Execute.Teams, types.StringValue)
24+
} else {
25+
state.Permissions.Execute.Users = utils.Map(utils.SortStringSliceByOther(a.Execute.Users, utils.TFStringListToStringArray(oldPermissions.Execute.Users)), types.StringValue)
26+
state.Permissions.Execute.Roles = utils.Map(utils.SortStringSliceByOther(a.Execute.Roles, utils.TFStringListToStringArray(oldPermissions.Execute.Roles)), types.StringValue)
27+
state.Permissions.Execute.Teams = utils.Map(utils.SortStringSliceByOther(a.Execute.Teams, utils.TFStringListToStringArray(oldPermissions.Execute.Teams)), types.StringValue)
3128
}
3229

3330
state.Permissions.Execute.OwnedByTeam = flex.GoBoolToFramework(a.Execute.OwnedByTeam)
@@ -43,19 +40,14 @@ func (r *ActionPermissionsResource) refreshActionPermissionsState(state *ActionP
4340

4441
state.Permissions.Approve = &ApproveModel{}
4542

46-
state.Permissions.Approve.Users = make([]types.String, len(a.Approve.Users))
47-
for i, u := range a.Approve.Users {
48-
state.Permissions.Approve.Users[i] = types.StringValue(u)
49-
}
50-
51-
state.Permissions.Approve.Roles = make([]types.String, len(a.Approve.Roles))
52-
for i, u := range a.Approve.Roles {
53-
state.Permissions.Approve.Roles[i] = types.StringValue(u)
54-
}
55-
56-
state.Permissions.Approve.Teams = make([]types.String, len(a.Approve.Teams))
57-
for i, u := range a.Approve.Teams {
58-
state.Permissions.Approve.Teams[i] = types.StringValue(u)
43+
if oldPermissions == nil || oldPermissions.Approve == nil {
44+
state.Permissions.Approve.Users = utils.Map(a.Approve.Users, types.StringValue)
45+
state.Permissions.Approve.Roles = utils.Map(a.Approve.Roles, types.StringValue)
46+
state.Permissions.Approve.Teams = utils.Map(a.Approve.Teams, types.StringValue)
47+
} else {
48+
state.Permissions.Approve.Users = utils.Map(utils.SortStringSliceByOther(a.Approve.Users, utils.TFStringListToStringArray(oldPermissions.Approve.Users)), types.StringValue)
49+
state.Permissions.Approve.Roles = utils.Map(utils.SortStringSliceByOther(a.Approve.Roles, utils.TFStringListToStringArray(oldPermissions.Approve.Roles)), types.StringValue)
50+
state.Permissions.Approve.Teams = utils.Map(utils.SortStringSliceByOther(a.Approve.Teams, utils.TFStringListToStringArray(oldPermissions.Approve.Teams)), types.StringValue)
5951
}
6052

6153
if a.Approve.Policy != nil {

port/blueprint-permissions/refreshBluePrintPermissionsToState.go

+105-22
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,112 @@
11
package blueprint_permissions
22

33
import (
4+
"github.com/port-labs/terraform-provider-port-labs/v2/internal/utils"
45
"strings"
56

67
"github.com/hashicorp/terraform-plugin-framework/types"
78
"github.com/port-labs/terraform-provider-port-labs/v2/internal/cli"
89
)
910

10-
func goStringListToTFList(list []string) []types.String {
11-
var result = make([]types.String, len(list))
12-
for i, u := range list {
13-
result[i] = types.StringValue(u)
14-
}
15-
16-
return result
17-
}
18-
19-
func blueprintPermissionsBlockToBlueprintPermissionsTFBlock(block cli.BlueprintPermissionsBlock) *BlueprintPermissionsTFBlock {
20-
return &BlueprintPermissionsTFBlock{
21-
Users: goStringListToTFList(block.Users),
22-
Roles: goStringListToTFList(block.Roles),
23-
Teams: goStringListToTFList(block.Teams),
24-
OwnedByTeam: types.BoolValue(*block.OwnedByTeam),
11+
func refreshBlueprintPermissionsState(state *BlueprintPermissionsModel, a *cli.BlueprintPermissions, blueprintId string) error {
12+
oldPermissions := state.Entities
13+
if oldPermissions == nil {
14+
oldPermissions = &EntitiesBlueprintPermissionsModel{}
2515
}
26-
}
2716

28-
func refreshBlueprintPermissionsState(state *BlueprintPermissionsModel, a *cli.BlueprintPermissions, blueprintId string) error {
2917
state.ID = types.StringValue(blueprintId)
3018
state.BlueprintIdentifier = types.StringValue(blueprintId)
3119
state.Entities = &EntitiesBlueprintPermissionsModel{}
32-
state.Entities.Update = blueprintPermissionsBlockToBlueprintPermissionsTFBlock(a.Entities.Update)
33-
state.Entities.Unregister = blueprintPermissionsBlockToBlueprintPermissionsTFBlock(a.Entities.Unregister)
34-
state.Entities.Register = blueprintPermissionsBlockToBlueprintPermissionsTFBlock(a.Entities.Register)
20+
21+
if oldPermissions.Update == nil {
22+
state.Entities.Update = &BlueprintPermissionsTFBlock{
23+
Users: utils.Map(a.Entities.Update.Users, types.StringValue),
24+
Roles: utils.Map(a.Entities.Update.Roles, types.StringValue),
25+
Teams: utils.Map(a.Entities.Update.Teams, types.StringValue),
26+
OwnedByTeam: types.BoolValue(*a.Entities.Update.OwnedByTeam),
27+
}
28+
} else {
29+
state.Entities.Update = &BlueprintPermissionsTFBlock{
30+
Users: utils.Map(utils.SortStringSliceByOther(a.Entities.Update.Users, utils.TFStringListToStringArray(oldPermissions.Update.Users)), types.StringValue),
31+
Roles: utils.Map(utils.SortStringSliceByOther(a.Entities.Update.Roles, utils.TFStringListToStringArray(oldPermissions.Update.Roles)), types.StringValue),
32+
Teams: utils.Map(utils.SortStringSliceByOther(a.Entities.Update.Teams, utils.TFStringListToStringArray(oldPermissions.Update.Teams)), types.StringValue),
33+
OwnedByTeam: types.BoolValue(*a.Entities.Update.OwnedByTeam),
34+
}
35+
}
36+
37+
if oldPermissions.Unregister == nil {
38+
state.Entities.Unregister = &BlueprintPermissionsTFBlock{
39+
Users: utils.Map(a.Entities.Unregister.Users, types.StringValue),
40+
Roles: utils.Map(a.Entities.Unregister.Roles, types.StringValue),
41+
Teams: utils.Map(a.Entities.Unregister.Teams, types.StringValue),
42+
OwnedByTeam: types.BoolValue(*a.Entities.Unregister.OwnedByTeam),
43+
}
44+
} else {
45+
state.Entities.Unregister = &BlueprintPermissionsTFBlock{
46+
Users: utils.Map(utils.SortStringSliceByOther(a.Entities.Unregister.Users, utils.TFStringListToStringArray(oldPermissions.Unregister.Users)), types.StringValue),
47+
Roles: utils.Map(utils.SortStringSliceByOther(a.Entities.Unregister.Roles, utils.TFStringListToStringArray(oldPermissions.Unregister.Roles)), types.StringValue),
48+
Teams: utils.Map(utils.SortStringSliceByOther(a.Entities.Unregister.Teams, utils.TFStringListToStringArray(oldPermissions.Unregister.Teams)), types.StringValue),
49+
OwnedByTeam: types.BoolValue(*a.Entities.Unregister.OwnedByTeam),
50+
}
51+
}
52+
53+
if oldPermissions.Register == nil {
54+
state.Entities.Register = &BlueprintPermissionsTFBlock{
55+
Users: utils.Map(a.Entities.Register.Users, types.StringValue),
56+
Roles: utils.Map(a.Entities.Register.Roles, types.StringValue),
57+
Teams: utils.Map(a.Entities.Register.Teams, types.StringValue),
58+
OwnedByTeam: types.BoolValue(*a.Entities.Register.OwnedByTeam),
59+
}
60+
} else {
61+
state.Entities.Register = &BlueprintPermissionsTFBlock{
62+
Users: utils.Map(utils.SortStringSliceByOther(a.Entities.Register.Users, utils.TFStringListToStringArray(oldPermissions.Register.Users)), types.StringValue),
63+
Roles: utils.Map(utils.SortStringSliceByOther(a.Entities.Register.Roles, utils.TFStringListToStringArray(oldPermissions.Register.Roles)), types.StringValue),
64+
Teams: utils.Map(utils.SortStringSliceByOther(a.Entities.Register.Teams, utils.TFStringListToStringArray(oldPermissions.Register.Teams)), types.StringValue),
65+
OwnedByTeam: types.BoolValue(*a.Entities.Register.OwnedByTeam),
66+
}
67+
}
68+
69+
if oldPermissions.UpdateProperties == nil {
70+
oldPermissions.UpdateProperties = &BlueprintRelationsPermissionsTFBlock{}
71+
}
3572

3673
state.Entities.UpdateProperties = nil
3774
var mappedUpdateProperties BlueprintRelationsPermissionsTFBlock = nil
3875
if len(a.Entities.UpdateProperties) > 0 {
3976
state.Entities.UpdateMetadataProperties = &BlueprintMetadataPermissionsTFBlock{}
4077
mappedUpdateProperties = make(BlueprintRelationsPermissionsTFBlock)
4178
for updatePropertyKey, updatePropertyValue := range a.Entities.UpdateProperties {
42-
var current = blueprintPermissionsBlockToBlueprintPermissionsTFBlock(updatePropertyValue)
79+
var oldPropValue *BlueprintPermissionsTFBlock
80+
if strings.HasPrefix(updatePropertyKey, "$") {
81+
switch updatePropertyKey {
82+
case "$title":
83+
oldPropValue = oldPermissions.UpdateMetadataProperties.Title
84+
case "$identifier":
85+
oldPropValue = oldPermissions.UpdateMetadataProperties.Identifier
86+
case "$icon":
87+
oldPropValue = oldPermissions.UpdateMetadataProperties.Icon
88+
case "$team":
89+
oldPropValue = oldPermissions.UpdateMetadataProperties.Team
90+
}
91+
} else if val, ok := (*oldPermissions.UpdateProperties)[updatePropertyKey]; ok {
92+
oldPropValue = &val
93+
}
94+
var current *BlueprintPermissionsTFBlock
95+
if oldPropValue == nil {
96+
current = &BlueprintPermissionsTFBlock{
97+
Users: utils.Map(updatePropertyValue.Users, types.StringValue),
98+
Roles: utils.Map(updatePropertyValue.Roles, types.StringValue),
99+
Teams: utils.Map(updatePropertyValue.Teams, types.StringValue),
100+
OwnedByTeam: types.BoolValue(*updatePropertyValue.OwnedByTeam),
101+
}
102+
} else {
103+
current = &BlueprintPermissionsTFBlock{
104+
Users: utils.Map(utils.SortStringSliceByOther(updatePropertyValue.Users, utils.TFStringListToStringArray(oldPropValue.Users)), types.StringValue),
105+
Roles: utils.Map(utils.SortStringSliceByOther(updatePropertyValue.Roles, utils.TFStringListToStringArray(oldPropValue.Roles)), types.StringValue),
106+
Teams: utils.Map(utils.SortStringSliceByOther(updatePropertyValue.Teams, utils.TFStringListToStringArray(oldPropValue.Teams)), types.StringValue),
107+
OwnedByTeam: types.BoolValue(*updatePropertyValue.OwnedByTeam),
108+
}
109+
}
43110

44111
if strings.HasPrefix(updatePropertyKey, "$") {
45112
switch updatePropertyKey {
@@ -60,10 +127,26 @@ func refreshBlueprintPermissionsState(state *BlueprintPermissionsModel, a *cli.B
60127
state.Entities.UpdateProperties = &mappedUpdateProperties
61128
}
62129
}
130+
63131
if len(a.Entities.UpdateRelations) > 0 {
64132
var mappedUpdateRelations = make(BlueprintRelationsPermissionsTFBlock, len(a.Entities.UpdateRelations))
65133
for updateRelationKey, updateRelationValue := range a.Entities.UpdateRelations {
66-
mappedUpdateRelations[updateRelationKey] = *blueprintPermissionsBlockToBlueprintPermissionsTFBlock(updateRelationValue)
134+
oldRelValue, hasOldRelValue := (*oldPermissions.UpdateRelations)[updateRelationKey]
135+
if !hasOldRelValue {
136+
mappedUpdateRelations[updateRelationKey] = BlueprintPermissionsTFBlock{
137+
Users: utils.Map(updateRelationValue.Users, types.StringValue),
138+
Roles: utils.Map(updateRelationValue.Roles, types.StringValue),
139+
Teams: utils.Map(updateRelationValue.Teams, types.StringValue),
140+
OwnedByTeam: types.BoolValue(*updateRelationValue.OwnedByTeam),
141+
}
142+
} else {
143+
mappedUpdateRelations[updateRelationKey] = BlueprintPermissionsTFBlock{
144+
Users: utils.Map(utils.SortStringSliceByOther(updateRelationValue.Users, utils.TFStringListToStringArray(oldRelValue.Users)), types.StringValue),
145+
Roles: utils.Map(utils.SortStringSliceByOther(updateRelationValue.Roles, utils.TFStringListToStringArray(oldRelValue.Roles)), types.StringValue),
146+
Teams: utils.Map(utils.SortStringSliceByOther(updateRelationValue.Teams, utils.TFStringListToStringArray(oldRelValue.Teams)), types.StringValue),
147+
OwnedByTeam: types.BoolValue(*updateRelationValue.OwnedByTeam),
148+
}
149+
}
67150
}
68151
state.Entities.UpdateRelations = &mappedUpdateRelations
69152
} else {

0 commit comments

Comments
 (0)