From d52d5a580d539d24214e278dd4cbbaaabe52438f Mon Sep 17 00:00:00 2001
From: Tom Tankilevitch <59158507+Tankilevitch@users.noreply.github.com>
Date: Mon, 29 Jan 2024 17:31:36 +0200
Subject: [PATCH 01/25] Support Force delete blueprint (#110)
---
docs/resources/port_action.md | 2 -
docs/resources/port_action_permissions.md | 2 -
docs/resources/port_aggregation_properties.md | 2 -
docs/resources/port_blueprint.md | 56 ++++-
docs/resources/port_entity.md | 2 -
docs/resources/port_scorecard.md | 202 +++++++++++++++++-
docs/resources/port_team.md | 2 -
docs/resources/port_webhook.md | 2 -
internal/cli/blueprint.go | 23 ++
internal/cli/migrations.go | 24 +++
internal/cli/models.go | 15 ++
internal/consts/migrations.go | 15 ++
port/blueprint/model.go | 1 +
port/blueprint/resource.go | 68 +++++-
port/blueprint/resource_test.go | 116 ++++++++++
port/blueprint/schema.go | 39 +++-
port/scorecard/schema.go | 166 +++++++-------
17 files changed, 631 insertions(+), 106 deletions(-)
create mode 100644 internal/cli/migrations.go
create mode 100644 internal/consts/migrations.go
diff --git a/docs/resources/port_action.md b/docs/resources/port_action.md
index d550a882..c4d1bf1d 100644
--- a/docs/resources/port_action.md
+++ b/docs/resources/port_action.md
@@ -429,5 +429,3 @@ Optional:
- `agent` (Boolean) Use the agent to invoke the action
- `method` (String) The HTTP method to invoke the action
- `synchronized` (Boolean) Synchronize the action
-
-
diff --git a/docs/resources/port_action_permissions.md b/docs/resources/port_action_permissions.md
index 00cb4cc0..ddebefb3 100644
--- a/docs/resources/port_action_permissions.md
+++ b/docs/resources/port_action_permissions.md
@@ -222,5 +222,3 @@ Optional:
- `roles` (List of String) The roles with execution permission
- `teams` (List of String) The teams with execution permission
- `users` (List of String) The users with execution permission
-
-
diff --git a/docs/resources/port_aggregation_properties.md b/docs/resources/port_aggregation_properties.md
index 35175153..9e7bb42c 100644
--- a/docs/resources/port_aggregation_properties.md
+++ b/docs/resources/port_aggregation_properties.md
@@ -645,5 +645,3 @@ Optional:
- `average_of` (String) The time periods to calculate the average of, e.g. hour, day, week, month
- `measure_time_by` (String) The property name on which to calculate the the time periods, e.g. $createdAt, $updated_at or any other date property
-
-
diff --git a/docs/resources/port_blueprint.md b/docs/resources/port_blueprint.md
index ceaa918c..13f0431e 100644
--- a/docs/resources/port_blueprint.md
+++ b/docs/resources/port_blueprint.md
@@ -97,6 +97,30 @@ description: |-
}
}
```
+ Force Deleting a Blueprint
+ There could be cases where a blueprint will be managed by Terraform, but entities will get created from other sources (e.g. Port UI, API or other supported integrations).
+ In this case, when trying to delete the blueprint, Terraform will fail because it will try to delete the blueprint without deleting the entities first as they are not managed by Terraform.
+ To overcome this behavior, you can set the argument force_delete_entities=true.
+ On the blueprint destroy it will trigger a migration that will delete all the entities in the blueprint and then delete the blueprint itself.
+ ```hcl
+ resource "portblueprint" "microservice" {
+ title = "Microservice"
+ icon = "Microservice"
+ identifier = "microservice"
+ properties = {
+ stringprops = {
+ "domain" = {
+ title = "Domain"
+ }
+ "slack-channel" = {
+ title = "Slack Channel"
+ format = "url"
+ }
+ }
+ }
+ forcedeleteentities = false
+ }
+ ```
---
# port_blueprint (Resource)
@@ -212,6 +236,35 @@ resource "port_blueprint" "microservice" {
```
+## Force Deleting a Blueprint
+
+There could be cases where a blueprint will be managed by Terraform, but entities will get created from other sources (e.g. Port UI, API or other supported integrations).
+In this case, when trying to delete the blueprint, Terraform will fail because it will try to delete the blueprint without deleting the entities first as they are not managed by Terraform.
+
+To overcome this behavior, you can set the argument `force_delete_entities=true`.
+On the blueprint destroy it will trigger a migration that will delete all the entities in the blueprint and then delete the blueprint itself.
+
+```hcl
+resource "port_blueprint" "microservice" {
+ title = "Microservice"
+ icon = "Microservice"
+ identifier = "microservice"
+ properties = {
+ string_props = {
+ "domain" = {
+ title = "Domain"
+ }
+ "slack-channel" = {
+ title = "Slack Channel"
+ format = "url"
+ }
+ }
+ }
+ force_delete_entities = false
+}
+
+```
+
@@ -226,6 +279,7 @@ resource "port_blueprint" "microservice" {
- `calculation_properties` (Attributes Map) The calculation properties of the blueprint (see [below for nested schema](#nestedatt--calculation_properties))
- `description` (String) The description of the blueprint
+- `force_delete_entities` (Boolean) If set to true, the blueprint will be deleted with all its entities, even if they are not managed by Terraform
- `icon` (String) The icon of the blueprint
- `kafka_changelog_destination` (Object) The changelog destination of the blueprint (see [below for nested schema](#nestedatt--kafka_changelog_destination))
- `mirror_properties` (Attributes Map) The mirror properties of the blueprint (see [below for nested schema](#nestedatt--mirror_properties))
@@ -444,5 +498,3 @@ Required:
Optional:
- `agent` (Boolean) The agent of the webhook changelog destination
-
-
diff --git a/docs/resources/port_entity.md b/docs/resources/port_entity.md
index a5524c46..5c1535ea 100644
--- a/docs/resources/port_entity.md
+++ b/docs/resources/port_entity.md
@@ -67,5 +67,3 @@ Optional:
- `many_relations` (Map of List of String) The many relation of the entity
- `single_relations` (Map of String) The single relation of the entity
-
-
diff --git a/docs/resources/port_scorecard.md b/docs/resources/port_scorecard.md
index 4db2ae0b..bc13c874 100644
--- a/docs/resources/port_scorecard.md
+++ b/docs/resources/port_scorecard.md
@@ -3,12 +3,208 @@
page_title: "port_scorecard Resource - terraform-provider-port-labs"
subcategory: ""
description: |-
- scorecard resource
+ Scorecard
+ This resource allows you to manage a scorecard.
+ See the Port documentation https://docs.getport.io/promote-scorecards/ for more information about scorecards.
+ Example Usage
+ Create a parent blueprint with a child blueprint and an aggregation property to count the parent kids:
+ ```hcl
+ resource "portblueprint" "microservice" {
+ title = "microservice"
+ icon = "Terraform"
+ identifier = "microservice"
+ properties = {
+ stringprops = {
+ "author" = {
+ title = "Author"
+ }
+ "url" = {
+ title = "URL"
+ }
+ }
+ booleanprops = {
+ "required" = {
+ type = "boolean"
+ }
+ }
+ numberprops = {
+ "sum" = {
+ type = "number"
+ }
+ }
+ }
+ }
+ resource "portscorecard" "readiness" {
+ identifier = "Readiness"
+ title = "Readiness"
+ blueprint = portblueprint.microservice.identifier
+ rules = [
+ {
+ identifier = "hasOwner"
+ title = "Has Owner"
+ level = "Gold"
+ query = {
+ combinator = "and"
+ conditions = [
+ jsonencode({
+ property = "$team"
+ operator = "isNotEmpty"
+ }),
+ jsonencode({
+ property = "author",
+ operator : "=",
+ value : "myValue"
+ })
+ ]
+ }
+ },
+ {
+ identifier = "hasUrl"
+ title = "Has URL"
+ level = "Silver"
+ query = {
+ combinator = "and"
+ conditions = [
+ jsonencode({
+ property = "url"
+ operator = "isNotEmpty"
+ })
+ ]
+ }
+ },
+ {
+ identifier = "checkSumIfRequired"
+ title = "Check Sum If Required"
+ level = "Bronze"
+ query = {
+ combinator = "or"
+ conditions = [
+ jsonencode({
+ property = "required"
+ operator : "="
+ value : false
+ }),
+ jsonencode({
+ property = "sum"
+ operator : ">"
+ value : 2
+ })
+ ]
+ }
+ }
+ ]
+ dependson = [
+ portblueprint.microservice
+ ]
+ }
+ ```
---
# port_scorecard (Resource)
-scorecard resource
+# Scorecard
+
+This resource allows you to manage a scorecard.
+
+See the [Port documentation](https://docs.getport.io/promote-scorecards/) for more information about scorecards.
+
+## Example Usage
+
+Create a parent blueprint with a child blueprint and an aggregation property to count the parent kids:
+
+```hcl
+
+resource "port_blueprint" "microservice" {
+ title = "microservice"
+ icon = "Terraform"
+ identifier = "microservice"
+ properties = {
+ string_props = {
+ "author" = {
+ title = "Author"
+ }
+ "url" = {
+ title = "URL"
+ }
+ }
+ boolean_props = {
+ "required" = {
+ type = "boolean"
+ }
+ }
+ number_props = {
+ "sum" = {
+ type = "number"
+ }
+ }
+ }
+}
+
+resource "port_scorecard" "readiness" {
+ identifier = "Readiness"
+ title = "Readiness"
+ blueprint = port_blueprint.microservice.identifier
+ rules = [
+ {
+ identifier = "hasOwner"
+ title = "Has Owner"
+ level = "Gold"
+ query = {
+ combinator = "and"
+ conditions = [
+ jsonencode({
+ property = "$team"
+ operator = "isNotEmpty"
+ }),
+ jsonencode({
+ property = "author",
+ operator : "=",
+ value : "myValue"
+ })
+ ]
+ }
+ },
+ {
+ identifier = "hasUrl"
+ title = "Has URL"
+ level = "Silver"
+ query = {
+ combinator = "and"
+ conditions = [
+ jsonencode({
+ property = "url"
+ operator = "isNotEmpty"
+ })
+ ]
+ }
+ },
+ {
+ identifier = "checkSumIfRequired"
+ title = "Check Sum If Required"
+ level = "Bronze"
+ query = {
+ combinator = "or"
+ conditions = [
+ jsonencode({
+ property = "required"
+ operator : "="
+ value : false
+ }),
+ jsonencode({
+ property = "sum"
+ operator : ">"
+ value : 2
+ })
+ ]
+ }
+ }
+ ]
+ depends_on = [
+ port_blueprint.microservice
+ ]
+}
+
+```
@@ -47,5 +243,3 @@ Required:
- `combinator` (String) The combinator of the query
- `conditions` (List of String) The conditions of the query. Each condition object should be encoded to a string
-
-
diff --git a/docs/resources/port_team.md b/docs/resources/port_team.md
index 59f9484b..518df378 100644
--- a/docs/resources/port_team.md
+++ b/docs/resources/port_team.md
@@ -30,5 +30,3 @@ Team resource
- `id` (String) The ID of this resource.
- `provider_name` (String) The provider of the team
- `updated_at` (String) The last update date of the team
-
-
diff --git a/docs/resources/port_webhook.md b/docs/resources/port_webhook.md
index 0e5c06ca..89654c52 100644
--- a/docs/resources/port_webhook.md
+++ b/docs/resources/port_webhook.md
@@ -75,5 +75,3 @@ Optional:
- `signature_algorithm` (String) The signature algorithm of the webhook
- `signature_header_name` (String) The signature header name of the webhook
- `signature_prefix` (String) The signature prefix of the webhook
-
-
diff --git a/internal/cli/blueprint.go b/internal/cli/blueprint.go
index 6de9f1cc..e6e88c1a 100644
--- a/internal/cli/blueprint.go
+++ b/internal/cli/blueprint.go
@@ -86,3 +86,26 @@ func (c *PortClient) DeleteBlueprint(ctx context.Context, id string) error {
}
return nil
}
+
+func (c *PortClient) DeleteBlueprintWithAllEntities(ctx context.Context, id string) (*string, error) {
+ url := "v1/blueprints/{identifier}/all-entities?delete_blueprint=true"
+ resp, err := c.Client.R().
+ SetContext(ctx).
+ SetHeader("Accept", "application/json").
+ SetPathParam("identifier", id).
+ Delete(url)
+ if err != nil {
+ return nil, err
+ }
+ var pb PortBody
+ err = json.Unmarshal(resp.Body(), &pb)
+ if err != nil {
+ return nil, err
+ }
+ if !pb.OK {
+ return nil, fmt.Errorf("failed to trigger blueprint deletion with all entities, got: %s", resp.Body())
+ }
+
+ return &pb.MigrationId, nil
+
+}
diff --git a/internal/cli/migrations.go b/internal/cli/migrations.go
new file mode 100644
index 00000000..2c7cc19f
--- /dev/null
+++ b/internal/cli/migrations.go
@@ -0,0 +1,24 @@
+package cli
+
+import (
+ "context"
+ "fmt"
+)
+
+func (c *PortClient) GetMigration(ctx context.Context, id string) (*Migration, error) {
+ pb := &PortBody{}
+ url := "v1/migrations/{identifier}"
+ resp, err := c.Client.R().
+ SetContext(ctx).
+ SetHeader("Accept", "application/json").
+ SetResult(pb).
+ SetPathParam("identifier", id).
+ Get(url)
+ if err != nil {
+ return nil, err
+ }
+ if !pb.OK {
+ return nil, fmt.Errorf("failed to read migration, got: %s", resp.Body())
+ }
+ return &pb.Migration, nil
+}
diff --git a/internal/cli/models.go b/internal/cli/models.go
index 9fbb720b..0d5f5dbc 100644
--- a/internal/cli/models.go
+++ b/internal/cli/models.go
@@ -295,6 +295,19 @@ type (
Users []string `json:"users,omitempty"`
Provider string `json:"provider,omitempty"`
}
+
+ Migration struct {
+ Meta
+ Id string `json:"id,omitempty"`
+ Actor string `json:"actor,omitempty"`
+ SourceBlueprint string `json:"sourceBlueprint,omitempty"`
+ Mapping any `json:"mapping,omitempty"`
+ Status string `json:"status,omitempty"`
+ DeleteBlueprint bool `json:"deleteBlueprint,omitempty"`
+ DeleteEntities bool `json:"deleteEntities,omitempty"`
+ FailureCount int `json:"failureCount,omitempty"`
+ SuccessCount int `json:"successCount,omitempty"`
+ }
)
type PortBody struct {
@@ -306,6 +319,8 @@ type PortBody struct {
Integration Webhook `json:"integration"`
Scorecard Scorecard `json:"Scorecard"`
Team Team `json:"team"`
+ MigrationId string `json:"migrationId"`
+ Migration Migration `json:"migration"`
}
type TeamUserBody struct {
diff --git a/internal/consts/migrations.go b/internal/consts/migrations.go
new file mode 100644
index 00000000..86117cf0
--- /dev/null
+++ b/internal/consts/migrations.go
@@ -0,0 +1,15 @@
+package consts
+
+const (
+ Failure = "FAILURE"
+ Cancelled = "CANCELLED"
+ Completed = "COMPLETED"
+ Running = "RUNNING"
+ Pending = "PENDING"
+ Initializing = "INITIALIZING"
+ PendingCancellation = "PENDING_CANCELLATION"
+)
+
+func IsTerminalStatus(status string) bool {
+ return status == Failure || status == Cancelled || status == Completed
+}
diff --git a/port/blueprint/model.go b/port/blueprint/model.go
index 4257b668..6da6d6f1 100644
--- a/port/blueprint/model.go
+++ b/port/blueprint/model.go
@@ -173,4 +173,5 @@ type BlueprintModel struct {
Relations map[string]RelationModel `tfsdk:"relations"`
MirrorProperties map[string]MirrorPropertyModel `tfsdk:"mirror_properties"`
CalculationProperties map[string]CalculationPropertyModel `tfsdk:"calculation_properties"`
+ ForceDeleteEntities types.Bool `tfsdk:"force_delete_entities"`
}
diff --git a/port/blueprint/resource.go b/port/blueprint/resource.go
index 25c19134..31ae7852 100644
--- a/port/blueprint/resource.go
+++ b/port/blueprint/resource.go
@@ -2,13 +2,16 @@ package blueprint
import (
"context"
-
+ "fmt"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/port-labs/terraform-provider-port-labs/internal/cli"
"github.com/port-labs/terraform-provider-port-labs/internal/consts"
"github.com/port-labs/terraform-provider-port-labs/internal/flex"
+ "strings"
+ "time"
)
var _ resource.Resource = &BlueprintResource{}
@@ -73,6 +76,10 @@ func refreshBlueprintState(ctx context.Context, bm *BlueprintModel, b *cli.Bluep
bm.Icon = flex.GoStringToFramework(b.Icon)
bm.Description = flex.GoStringToFramework(b.Description)
+ if bm.ForceDeleteEntities.IsNull() {
+ bm.ForceDeleteEntities = types.BoolValue(false)
+ }
+
if b.ChangelogDestination != nil {
if b.ChangelogDestination.Type == consts.Kafka {
bm.KafkaChangelogDestination, _ = types.ObjectValue(nil, nil)
@@ -146,6 +153,10 @@ func writeBlueprintComputedFieldsToState(state *BlueprintModel, bp *cli.Blueprin
state.CreatedBy = types.StringValue(bp.CreatedBy)
state.UpdatedAt = types.StringValue(bp.UpdatedAt.String())
state.UpdatedBy = types.StringValue(bp.UpdatedBy)
+
+ if state.ForceDeleteEntities.IsNull() {
+ state.ForceDeleteEntities = types.BoolValue(false)
+ }
}
func (r *BlueprintResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
@@ -212,12 +223,24 @@ func (r *BlueprintResource) Delete(ctx context.Context, req resource.DeleteReque
return
}
- err := r.portClient.DeleteBlueprint(ctx, state.Identifier.ValueString())
+ // if deletion protection is not set, this means that the user destroyed the resource, right after upgrading to a version that supports deletion protection
+ // therefor we want to be backwards compatible and assume that the user want to have deletion protection
+ forceDeleteEntities := state.ForceDeleteEntities.ValueBool()
- if err != nil {
- resp.Diagnostics.AddError("failed to delete blueprint", err.Error())
- return
+ if !forceDeleteEntities {
+ err := r.portClient.DeleteBlueprint(ctx, state.Identifier.ValueString())
+ if err != nil {
+ if strings.Contains(err.Error(), "has_dependents") {
+ resp.Diagnostics.AddError("failed to delete blueprint", fmt.Sprintf(`Blueprint %s has dependant entities that aren't managed by terraform, if you still wish to destroy the blueprint and delete all entities, set the force_delete_entities argument to true`, state.Identifier.ValueString()))
+ return
+ }
+ resp.Diagnostics.AddError("failed to delete blueprint", err.Error())
+ return
+ }
+ } else {
+ forceDeleteBlueprint(ctx, r.portClient, state, resp)
}
+
}
func (r *BlueprintResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
@@ -231,6 +254,41 @@ func (r *BlueprintResource) ImportState(ctx context.Context, req resource.Import
)...)
}
+func forceDeleteBlueprint(ctx context.Context, portClient *cli.PortClient, state *BlueprintModel, resp *resource.DeleteResponse) {
+ migrationId, err := portClient.DeleteBlueprintWithAllEntities(ctx, state.Identifier.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("failed to delete blueprint", err.Error())
+ return
+ }
+ // query migration status until status is SUCCESS or FAILED
+ for {
+ migration, err := portClient.GetMigration(ctx, *migrationId)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to get migration status", err.Error())
+ return
+ }
+ if migration.Status == consts.Failure {
+ resp.Diagnostics.AddError("failed to delete blueprint", "migration failed")
+ return
+ }
+ if migration.Status == consts.Cancelled {
+ resp.Diagnostics.AddError("failed to delete blueprint", "migration was cancelled")
+ return
+ }
+ if migration.Status == consts.Completed {
+ tflog.Info(ctx, "Migration completed successfully", map[string]interface{}{
+ "migration_id": migration.Id,
+ })
+ break
+ }
+ if err != nil {
+ resp.Diagnostics.AddError("failed to get migration status", err.Error())
+ return
+ }
+ time.Sleep(5 * time.Second)
+ }
+}
+
func blueprintResourceToPortRequest(ctx context.Context, state *BlueprintModel) (*cli.Blueprint, error) {
b := &cli.Blueprint{
Identifier: state.Identifier.ValueString(),
diff --git a/port/blueprint/resource_test.go b/port/blueprint/resource_test.go
index b7c04011..66fc4694 100644
--- a/port/blueprint/resource_test.go
+++ b/port/blueprint/resource_test.go
@@ -1,7 +1,13 @@
package blueprint_test
import (
+ "context"
"fmt"
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+ "github.com/port-labs/terraform-provider-port-labs/internal/consts"
+ "github.com/port-labs/terraform-provider-port-labs/version"
+ "os"
+ "regexp"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -722,3 +728,113 @@ func TestAccPortUpdateBlueprintIdentifier(t *testing.T) {
},
})
}
+
+func TestAccPortDestroyDeleteAllEntities(t *testing.T) {
+ identifier := utils.GenID()
+ title := "Blueprint with entities1"
+ icon := "Terraform"
+ var testAccBlueprintConfigImport = fmt.Sprintf(`
+ resource "port_blueprint" "microservice" {
+ identifier = "%s"
+ icon = "%s"
+ title = "%s"
+ }
+ `, identifier, icon, title)
+
+ var testAccBlueprintConfigForceDeleteEntitiesTrue = fmt.Sprintf(`
+ resource "port_blueprint" "microservice" {
+ identifier = "%s"
+ icon = "%s"
+ title = "%s"
+ force_delete_entities = true
+ }
+ `, identifier, icon, title)
+
+ portClient, ctx, err := initializePortTestClient(t)
+ if err != nil {
+ t.Fatalf("Failed to initialize port client: %s", err.Error())
+ return
+ }
+
+ blueprint := &cli.Blueprint{
+ Identifier: identifier,
+ Icon: &icon,
+ Title: title,
+ Schema: cli.BlueprintSchema{
+ Properties: map[string]cli.BlueprintProperty{},
+ },
+ CalculationProperties: map[string]cli.BlueprintCalculationProperty{},
+ AggregationProperties: map[string]cli.BlueprintAggregationProperty{},
+ MirrorProperties: map[string]cli.BlueprintMirrorProperty{},
+ Relations: map[string]cli.Relation{},
+ }
+
+ _, err = portClient.CreateBlueprint(ctx, blueprint)
+
+ if err != nil {
+ t.Fatalf("Failed to create blueprint: %s", err.Error())
+ return
+ }
+
+ entity := &cli.Entity{
+ Blueprint: identifier,
+ Properties: map[string]interface{}{},
+ Relations: map[string]any{},
+ }
+
+ // create entity
+ _, err = portClient.CreateEntity(ctx, entity, "")
+ if err != nil {
+ t.Fatalf("Failed to create entity: %s", err.Error())
+ return
+ }
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccBlueprintConfigImport,
+ ResourceName: "port_blueprint.microservice",
+ ImportStateId: identifier,
+ ImportState: true,
+ ImportStatePersist: true,
+ },
+ {
+ Config: acctest.ProviderConfig + testAccBlueprintConfigImport,
+ Destroy: true,
+ ExpectError: regexp.MustCompile(".* has dependant entities*"),
+ },
+ {
+ Config: acctest.ProviderConfig + testAccBlueprintConfigForceDeleteEntitiesTrue,
+ Check: resource.TestCheckResourceAttr("port_blueprint.microservice", "force_delete_entities", "true"),
+ },
+ {
+ Config: acctest.ProviderConfig + testAccBlueprintConfigForceDeleteEntitiesTrue,
+ Destroy: true,
+ },
+ },
+ })
+
+}
+
+func initializePortTestClient(t *testing.T) (*cli.PortClient, context.Context, error) {
+ baseUrl := os.Getenv("PORT_BASE_URL")
+ clientId := os.Getenv("PORT_CLIENT_ID")
+ clientSecret := os.Getenv("PORT_CLIENT_SECRET")
+
+ if baseUrl == "" {
+ baseUrl = consts.DefaultBaseUrl
+ }
+ c, err := cli.New(baseUrl, cli.WithHeader("User-Agent", version.ProviderVersion))
+ if err != nil {
+ t.Fatalf("Failed to create Port-labs client: %s", err.Error())
+ }
+ ctx := context.Background()
+ _, err = c.Authenticate(ctx, clientId, clientSecret)
+ if err != nil {
+ t.Fatalf("Failed to authenticate with Port-labs: %s", err.Error())
+ return nil, ctx, err
+ }
+ return c, ctx, nil
+}
diff --git a/port/blueprint/schema.go b/port/blueprint/schema.go
index d5a86fae..f789e9b7 100644
--- a/port/blueprint/schema.go
+++ b/port/blueprint/schema.go
@@ -447,6 +447,12 @@ func BlueprintSchema() map[string]schema.Attribute {
},
},
},
+ "force_delete_entities": schema.BoolAttribute{
+ MarkdownDescription: "If set to true, the blueprint will be deleted with all its entities, even if they are not managed by Terraform",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ },
}
}
@@ -568,4 +574,35 @@ resource "port_blueprint" "microservice" {
}
}
-` + "```" + ``
+` + "```" + `
+
+## Force Deleting a Blueprint
+
+There could be cases where a blueprint will be managed by Terraform, but entities will get created from other sources (e.g. Port UI, API or other supported integrations).
+In this case, when trying to delete the blueprint, Terraform will fail because it will try to delete the blueprint without deleting the entities first as they are not managed by Terraform.
+
+To overcome this behavior, you can set the argument ` + "`force_delete_entities=true`" + `.
+On the blueprint destroy it will trigger a migration that will delete all the entities in the blueprint and then delete the blueprint itself.
+
+` + "```hcl" + `
+resource "port_blueprint" "microservice" {
+ title = "Microservice"
+ icon = "Microservice"
+ identifier = "microservice"
+ properties = {
+ string_props = {
+ "domain" = {
+ title = "Domain"
+ }
+ "slack-channel" = {
+ title = "Slack Channel"
+ format = "url"
+ }
+ }
+ }
+ force_delete_entities = false
+}
+
+` + "```" + `
+
+`
diff --git a/port/scorecard/schema.go b/port/scorecard/schema.go
index 0b3098df..8e49f284 100644
--- a/port/scorecard/schema.go
+++ b/port/scorecard/schema.go
@@ -124,91 +124,93 @@ Create a parent blueprint with a child blueprint and an aggregation property to
` + "```hcl" + `
resource "port_blueprint" "microservice" {
- title = "microservice"
- icon = "Terraform"
- identifier = "microservice"
- properties = {
- string_props = {
- "author" = {
- title = "Author"
- }
- "url" = {
- title = "URL"
- }
- }
- boolean_props = {
- "required" = {
- type = "boolean"
- }
- }
- number_props = {
- "sum" = {
- type = "number"
- }
- }
- }
+ title = "microservice"
+ icon = "Terraform"
+ identifier = "microservice"
+ properties = {
+ string_props = {
+ "author" = {
+ title = "Author"
+ }
+ "url" = {
+ title = "URL"
+ }
+ }
+ boolean_props = {
+ "required" = {
+ type = "boolean"
+ }
+ }
+ number_props = {
+ "sum" = {
+ type = "number"
+ }
+ }
+ }
}
resource "port_scorecard" "readiness" {
- identifier = "Readiness"
- title = "Readiness"
- blueprint = port_blueprint.microservice.identifier
- rules = [
- {
- identifier = "hasOwner"
- title = "Has Owner"
- level = "Gold"
- query = {
- combinator = "and"
- conditions = [
- jsonencode({
- property = "$team"
- operator = "isNotEmpty"
- }),
- jsonencode({
- property = "author",
- operator : "=",
- value : "myValue"
- })
- ]
- }
- },
- {
- identifier = "hasUrl"
- title = "Has URL"
- level = "Silver"
- query = {
- combinator = "and"
- conditions = [jsonencode({
- property = "url"
- operator = "isNotEmpty"
- })]
- }
- },
- {
- identifier = "checkSumIfRequired"
- title = "Check Sum If Required"
- level = "Bronze"
- query = {
- combinator = "or"
- conditions = [
- jsonencode({
- property = "required"
- operator : "="
- value : false
- }),
- jsonencode({
- property = "sum"
- operator : ">"
- value : 2
- })
- ]
- }
- }
- ]
- depends_on = [
- port_blueprint.microservice
- ]
+ identifier = "Readiness"
+ title = "Readiness"
+ blueprint = port_blueprint.microservice.identifier
+ rules = [
+ {
+ identifier = "hasOwner"
+ title = "Has Owner"
+ level = "Gold"
+ query = {
+ combinator = "and"
+ conditions = [
+ jsonencode({
+ property = "$team"
+ operator = "isNotEmpty"
+ }),
+ jsonencode({
+ property = "author",
+ operator : "=",
+ value : "myValue"
+ })
+ ]
+ }
+ },
+ {
+ identifier = "hasUrl"
+ title = "Has URL"
+ level = "Silver"
+ query = {
+ combinator = "and"
+ conditions = [
+ jsonencode({
+ property = "url"
+ operator = "isNotEmpty"
+ })
+ ]
+ }
+ },
+ {
+ identifier = "checkSumIfRequired"
+ title = "Check Sum If Required"
+ level = "Bronze"
+ query = {
+ combinator = "or"
+ conditions = [
+ jsonencode({
+ property = "required"
+ operator : "="
+ value : false
+ }),
+ jsonencode({
+ property = "sum"
+ operator : ">"
+ value : 2
+ })
+ ]
+ }
+ }
+ ]
+ depends_on = [
+ port_blueprint.microservice
+ ]
}
` + "```"
From 185f5fc6c9a3d6c5efc14f99edeca1b9260f4d9d Mon Sep 17 00:00:00 2001
From: Tom Tankilevitch <59158507+Tankilevitch@users.noreply.github.com>
Date: Mon, 29 Jan 2024 18:36:45 +0200
Subject: [PATCH 02/25] Action Permissions - Fix fields to required (#111)
---
port/action-permissions/resource.go | 8 +----
port/action-permissions/resource_test.go | 38 +++++++++++++++++++++++-
port/action-permissions/schema.go | 23 ++++++++++++--
3 files changed, 58 insertions(+), 11 deletions(-)
diff --git a/port/action-permissions/resource.go b/port/action-permissions/resource.go
index 5d30804c..f0278258 100644
--- a/port/action-permissions/resource.go
+++ b/port/action-permissions/resource.go
@@ -96,19 +96,13 @@ func (r *ActionPermissionsResource) Update(ctx context.Context, req resource.Upd
return
}
- a, err := r.portClient.UpdateActionPermissions(ctx, blueprintIdentifier, actionIdentifier, permissions)
+ _, err = r.portClient.UpdateActionPermissions(ctx, blueprintIdentifier, actionIdentifier, permissions)
if err != nil {
resp.Diagnostics.AddError("failed to update action permissions", err.Error())
return
}
- err = refreshActionPermissionsState(ctx, state, a, blueprintIdentifier, actionIdentifier)
- if err != nil {
- resp.Diagnostics.AddError("failed to refresh action permissions state", err.Error())
- return
- }
-
state.ID = types.StringValue(fmt.Sprintf("%s:%s", blueprintIdentifier, actionIdentifier))
state.ActionIdentifier = types.StringValue(actionIdentifier)
state.BlueprintIdentifier = types.StringValue(blueprintIdentifier)
diff --git a/port/action-permissions/resource_test.go b/port/action-permissions/resource_test.go
index d6f38e44..06b5fcfc 100644
--- a/port/action-permissions/resource_test.go
+++ b/port/action-permissions/resource_test.go
@@ -465,7 +465,6 @@ func TestAccPortActionPermissionsImportState(t *testing.T) {
value: "true",
operator: "=",
property: "$owned_by_team"
-
}
],
combinator: "and"
@@ -505,3 +504,40 @@ func TestAccPortActionPermissionsImportState(t *testing.T) {
},
})
}
+
+func TestAccPortActionWithEmptyFieldsExpectDefaultsToApply(t *testing.T) {
+ blueprintIdentifier := utils.GenID()
+ actionIdentifier := utils.GenID()
+ var testAccActionPermissionsConfigCreate = testAccCreateBlueprintAndActionConfig(blueprintIdentifier, actionIdentifier) + `
+ resource "port_action_permissions" "create_microservice_permissions" {
+ action_identifier = port_action.create_microservice.identifier
+ blueprint_identifier = port_blueprint.microservice.identifier
+ permissions = {
+ "execute": {}
+ "approve": {}
+ }
+ }
+`
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccActionPermissionsConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_action_permissions.create_microservice_permissions", "action_identifier", actionIdentifier),
+ resource.TestCheckResourceAttr("port_action_permissions.create_microservice_permissions", "blueprint_identifier", blueprintIdentifier),
+ resource.TestCheckResourceAttr("port_action_permissions.create_microservice_permissions", "permissions.execute.roles.#", "0"),
+ resource.TestCheckResourceAttr("port_action_permissions.create_microservice_permissions", "permissions.execute.users.#", "0"),
+ resource.TestCheckResourceAttr("port_action_permissions.create_microservice_permissions", "permissions.execute.teams.#", "0"),
+ resource.TestCheckResourceAttr("port_action_permissions.create_microservice_permissions", "permissions.execute.owned_by_team", "true"),
+ resource.TestCheckNoResourceAttr("port_action_permissions.create_microservice_permissions", "permissions.approve.policy"),
+ resource.TestCheckResourceAttr("port_action_permissions.create_microservice_permissions", "permissions.approve.roles.#", "0"),
+ resource.TestCheckResourceAttr("port_action_permissions.create_microservice_permissions", "permissions.approve.users.#", "0"),
+ resource.TestCheckResourceAttr("port_action_permissions.create_microservice_permissions", "permissions.approve.teams.#", "0"),
+ resource.TestCheckNoResourceAttr("port_action_permissions.create_microservice_permissions", "permissions.approve.policy"),
+ ),
+ },
+ },
+ })
+}
diff --git a/port/action-permissions/schema.go b/port/action-permissions/schema.go
index 9b7690c0..f64c7718 100644
--- a/port/action-permissions/schema.go
+++ b/port/action-permissions/schema.go
@@ -2,8 +2,11 @@ package action_permissions
import (
"context"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -22,30 +25,38 @@ func ActionPermissionsSchema() map[string]schema.Attribute {
},
"permissions": schema.SingleNestedAttribute{
MarkdownDescription: "The permissions for the action",
- Optional: true,
+ Required: true,
Attributes: map[string]schema.Attribute{
"execute": schema.SingleNestedAttribute{
MarkdownDescription: "The permission to execute the action",
- Optional: true,
+ Required: true,
Attributes: map[string]schema.Attribute{
"users": schema.ListAttribute{
MarkdownDescription: "The users with execution permission",
Optional: true,
+ Computed: true,
+ Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})),
ElementType: types.StringType,
},
"roles": schema.ListAttribute{
MarkdownDescription: "The roles with execution permission",
Optional: true,
+ Computed: true,
+ Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})),
ElementType: types.StringType,
},
"teams": schema.ListAttribute{
MarkdownDescription: "The teams with execution permission",
Optional: true,
+ Computed: true,
+ Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})),
ElementType: types.StringType,
},
"owned_by_team": schema.BoolAttribute{
MarkdownDescription: "Give execution permission to the teams who own the entity",
Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(true),
},
"policy": schema.StringAttribute{
MarkdownDescription: "The policy to use for execution",
@@ -55,21 +66,27 @@ func ActionPermissionsSchema() map[string]schema.Attribute {
},
"approve": schema.SingleNestedAttribute{
MarkdownDescription: "The permission to approve the action's runs",
- Optional: true,
+ Required: true,
Attributes: map[string]schema.Attribute{
"users": schema.ListAttribute{
MarkdownDescription: "The users with approval permission",
Optional: true,
+ Computed: true,
+ Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})),
ElementType: types.StringType,
},
"roles": schema.ListAttribute{
MarkdownDescription: "The roles with approval permission",
Optional: true,
+ Computed: true,
+ Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})),
ElementType: types.StringType,
},
"teams": schema.ListAttribute{
MarkdownDescription: "The teams with approval permission",
Optional: true,
+ Computed: true,
+ Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})),
ElementType: types.StringType,
},
"policy": schema.StringAttribute{
From e6c30b48de374a3253dfcd95f02762cea966d436 Mon Sep 17 00:00:00 2001
From: Tom Tankilevitch <59158507+Tankilevitch@users.noreply.github.com>
Date: Mon, 19 Feb 2024 16:50:05 +0200
Subject: [PATCH 03/25] Support Page Resource (#109)
---
.github/workflows/ci.yml | 2 +
Makefile | 2 +-
docs/resources/port_action_permissions.md | 5 +-
docs/resources/port_page.md | 476 ++++++++++++++++++++++
examples/resources/port_page/main.tf | 62 +++
internal/cli/models.go | 14 +
internal/cli/page.go | 91 +++++
internal/utils/utils.go | 2 +-
port/page/model.go | 20 +
port/page/pageToPortBody.go | 44 ++
port/page/refreshPageToState.go | 32 ++
port/page/resource.go | 178 ++++++++
port/page/resource_test.go | 307 ++++++++++++++
port/page/schema.go | 351 ++++++++++++++++
port/webhook/resource_test.go | 32 +-
provider/provider.go | 2 +
16 files changed, 1597 insertions(+), 23 deletions(-)
create mode 100644 docs/resources/port_page.md
create mode 100644 examples/resources/port_page/main.tf
create mode 100644 internal/cli/page.go
create mode 100644 port/page/model.go
create mode 100644 port/page/pageToPortBody.go
create mode 100644 port/page/refreshPageToState.go
create mode 100644 port/page/resource.go
create mode 100644 port/page/resource_test.go
create mode 100644 port/page/schema.go
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0515cc7c..2d42e357 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,6 +29,8 @@ jobs:
run: make setup
acctest:
+ concurrency:
+ group: acctest
runs-on: ubuntu-20.04
strategy:
matrix:
diff --git a/Makefile b/Makefile
index fcf49bb0..c512fee7 100644
--- a/Makefile
+++ b/Makefile
@@ -40,7 +40,7 @@ setup:
cd tools && go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs
acctest:
- TF_ACC=1 PORT_CLIENT_ID=$(PORT_CLIENT_ID) PORT_CLIENT_SECRET=$(PORT_CLIENT_SECRET) go test ./...
+ TF_ACC=1 PORT_CLIENT_ID=$(PORT_CLIENT_ID) PORT_CLIENT_SECRET=$(PORT_CLIENT_SECRET) go test -p 1 ./...
gen-docs:
tfplugindocs
diff --git a/docs/resources/port_action_permissions.md b/docs/resources/port_action_permissions.md
index ddebefb3..b92aac46 100644
--- a/docs/resources/port_action_permissions.md
+++ b/docs/resources/port_action_permissions.md
@@ -184,9 +184,6 @@ resource "port_action_permissions" "restart_microservice_permissions" {
- `action_identifier` (String) The ID of the action
- `blueprint_identifier` (String) The ID of the blueprint
-
-### Optional
-
- `permissions` (Attributes) The permissions for the action (see [below for nested schema](#nestedatt--permissions))
### Read-Only
@@ -196,7 +193,7 @@ resource "port_action_permissions" "restart_microservice_permissions" {
### Nested Schema for `permissions`
-Optional:
+Required:
- `approve` (Attributes) The permission to approve the action's runs (see [below for nested schema](#nestedatt--permissions--approve))
- `execute` (Attributes) The permission to execute the action (see [below for nested schema](#nestedatt--permissions--execute))
diff --git a/docs/resources/port_page.md b/docs/resources/port_page.md
new file mode 100644
index 00000000..13893a43
--- /dev/null
+++ b/docs/resources/port_page.md
@@ -0,0 +1,476 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "port_page Resource - terraform-provider-port-labs"
+subcategory: ""
+description: |-
+ Page resource
+ Docs about the different page types can be found here https://docs.getport.io/customize-pages-dashboards-and-plugins/page/catalog-page.
+ ~> WARNINGThe page resource is currently in beta and is subject to change in future versions.Use it by setting the Environment Variable PORT_BETA_FEATURES_ENABLED=true.If this Environment Variable isn't specified, you won't be able to use the resource.
+ Example Usage
+ Blueprint Entities Page
+ ```hcl
+ resource "portpage" "microserviceblueprintpage" {
+ identifier = "microserviceblueprintpage"
+ title = "Microservices"
+ type = "blueprint-entities"
+ icon = "Microservice"
+ blueprint = portblueprint.base_blueprint.identifier
+ widgets = [
+ jsonencode(
+ {
+ "id" : "microservice-table-entities",
+ "type" : "table-entities-explorer",
+ "dataset" : {
+ "combinator" : "and",
+ "rules" : [
+ {
+ "operator" : "=",
+ "property" : "$blueprint",
+ "value" : {{blueprint}}
+ }
+ ]
+ }
+ }
+ )
+ ]
+ }
+ ```
+ Dashboard Page
+ ```hcl
+ resource "portpage" "microservicedashboardpage" {
+ identifier = "microservicedashboard_page"
+ title = "Microservices"
+ icon = "GitHub"
+ type = "dashboard"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+ }
+ ```
+ Page with parent
+ Create a page inside a folder.
+ ```hcl
+ resource "portpage" "microservicedashboardpage" {
+ identifier = "microservicedashboard_page"
+ title = "Microservices"
+ icon = "GitHub"
+ type = "dashboard"
+ parent = "microservices-folder"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+ }
+ ```
+ Page with after
+ Create a page after another page.
+ ```hcl
+ resource "portpage" "microservicedashboardpage" {
+ identifier = "microservicedashboardpage"
+ title = "Microservices"
+ icon = "GitHub"
+ type = "dashboard"
+ after = "microservicesentities_page"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+ }
+ ```
+ Home Page
+ ```hcl
+ resource "portpage" "homepage" {
+ identifier = "$home"
+ title = "Home"
+ type = "home"
+ widgets = [
+ jsonencode(
+ {
+ "type" : "dashboard-widget",
+ "id" : "azkLJD6wLk6nJSvA",
+ "layout" : [
+ {
+ "columns" : [
+ {
+ "id" : "markdown",
+ "size" : 6
+ },
+ {
+ "id" : "overview",
+ "size" : 6
+ },
+ ],
+ "height" : 648
+ }
+ ],
+ "widgets" : [
+ {
+ "type" : "markdown",
+ "markdown" : "## Welcome to your internal developer portal",
+ "icon" : "port",
+ "title" : "About developer Portal",
+ "id" : "markdown"
+ },
+ {
+ "type" : "iframe-widget",
+ "id" : "overview",
+ "title" : "Overview",
+ "icon" : "Docs",
+ "url" : "https://www.youtube.com/embed/ggXL2ZsPVQM?si=xj6XtV0faatoOhss",
+ "urlType" : "public"
+ }
+ ]
+ }
+ )
+ ]
+ }
+ ```
+ The home page is a special page, which is created by default when you create a new organization.
+ When deleting the home page resource using terraform, the home page will not be deleted from Port as it isn't deletable page, instead, the home page will be removed from the terraform state.Due to only having one home page you'll have to import the state of the home page manually.
+
+ terraform import port_page.home_page "\$home"
+---
+
+# port_page (Resource)
+
+# Page resource
+
+Docs about the different page types can be found [here](https://docs.getport.io/customize-pages-dashboards-and-plugins/page/catalog-page).
+
+~> **WARNING**
+The page resource is currently in beta and is subject to change in future versions.
+Use it by setting the Environment Variable `PORT_BETA_FEATURES_ENABLED=true`.
+If this Environment Variable isn't specified, you won't be able to use the resource.
+
+## Example Usage
+
+### Blueprint Entities Page
+
+```hcl
+
+resource "port_page" "microservice_blueprint_page" {
+ identifier = "microservice_blueprint_page"
+ title = "Microservices"
+ type = "blueprint-entities"
+ icon = "Microservice"
+ blueprint = port_blueprint.base_blueprint.identifier
+ widgets = [
+ jsonencode(
+ {
+ "id" : "microservice-table-entities",
+ "type" : "table-entities-explorer",
+ "dataset" : {
+ "combinator" : "and",
+ "rules" : [
+ {
+ "operator" : "=",
+ "property" : "$blueprint",
+ "value" : "{{blueprint}}"
+ }
+ ]
+ }
+ }
+ )
+ ]
+}
+
+```
+
+### Dashboard Page
+
+```hcl
+
+resource "port_page" "microservice_dashboard_page" {
+ identifier = "microservice_dashboard_page"
+ title = "Microservices"
+ icon = "GitHub"
+ type = "dashboard"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
+
+```
+
+
+### Page with parent
+
+Create a page inside a folder.
+
+```hcl
+
+resource "port_page" "microservice_dashboard_page" {
+ identifier = "microservice_dashboard_page"
+ title = "Microservices"
+ icon = "GitHub"
+ type = "dashboard"
+ parent = "microservices-folder"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
+
+```
+
+
+### Page with after
+
+Create a page after another page.
+
+```hcl
+
+resource "port_page" "microservice_dashboard_page" {
+ identifier = "microservice_dashboard_page"
+ title = "Microservices"
+ icon = "GitHub"
+ type = "dashboard"
+ after = "microservices_entities_page"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
+
+```
+
+### Home Page
+
+```hcl
+
+resource "port_page" "home_page" {
+ identifier = "$home"
+ title = "Home"
+ type = "home"
+ widgets = [
+ jsonencode(
+ {
+ "type" : "dashboard-widget",
+ "id" : "azkLJD6wLk6nJSvA",
+ "layout" : [
+ {
+ "columns" : [
+ {
+ "id" : "markdown",
+ "size" : 6
+ },
+ {
+ "id" : "overview",
+ "size" : 6
+ },
+ ],
+ "height" : 648
+ }
+ ],
+ "widgets" : [
+ {
+ "type" : "markdown",
+ "markdown" : "## Welcome to your internal developer portal",
+ "icon" : "port",
+ "title" : "About developer Portal",
+ "id" : "markdown"
+ },
+ {
+ "type" : "iframe-widget",
+ "id" : "overview",
+ "title" : "Overview",
+ "icon" : "Docs",
+ "url" : "https://www.youtube.com/embed/ggXL2ZsPVQM?si=xj6XtV0faatoOhss",
+ "urlType" : "public"
+ }
+ ]
+ }
+ )
+ ]
+}
+
+```
+
+The home page is a special page, which is created by default when you create a new organization.
+
+- When deleting the home page resource using terraform, the home page will not be deleted from Port as it isn't deletable page, instead, the home page will be removed from the terraform state.
+- Due to only having one home page you'll have to import the state of the home page manually.
+
+```
+terraform import port_page.home_page "\$home"
+```
+
+
+
+
+## Schema
+
+### Required
+
+- `identifier` (String) The Identifier of the page
+- `type` (String) The type of the page, can be one of "blueprint-entities", "dashboard" or "home"
+
+### Optional
+
+- `after` (String) The identifier of the page/folder after which the page should be placed
+- `blueprint` (String) The blueprint for which the page is created, relevant only for pages of type "blueprint-entities"
+- `icon` (String) The icon of the page
+- `locked` (Boolean) Whether the page is locked, if true, viewers will not be able to edit the page widgets and filters
+- `parent` (String) The identifier of the folder in which the page is in, default is the root of the sidebar
+- `title` (String) The title of the page
+- `widgets` (List of String) The widgets of the page
+
+### Read-Only
+
+- `created_at` (String) The creation date of the page
+- `created_by` (String) The creator of the page
+- `id` (String) The ID of this resource.
+- `updated_at` (String) The last update date of the page
+- `updated_by` (String) The last updater of the page
diff --git a/examples/resources/port_page/main.tf b/examples/resources/port_page/main.tf
new file mode 100644
index 00000000..1857fec5
--- /dev/null
+++ b/examples/resources/port_page/main.tf
@@ -0,0 +1,62 @@
+resource "port_page" "microservice_blueprint_page" {
+ identifier = "microservice_blueprint_page"
+ title = "Microservices"
+ type = "blueprint-entities"
+ icon = "Microservice"
+ blueprint = port_blueprint.base_blueprint.identifier
+ widgets = [
+ jsonencode(
+ {
+ "id" : "microservice-table-entities",
+ "type" : "table-entities-explorer",
+ "dataset" : {
+ "combinator" : "and",
+ "rules" : [
+ {
+ "operator" : "=",
+ "property" : "$blueprint",
+ "value" : "{{`\"{{blueprint}}\"`}}"
+ }
+ ]
+ }
+ }
+ )
+ ]
+}
+
+
+resource "port_page" "microservice_dashboard_page" {
+ identifier = "microservice_dashboard_page"
+ title = "Microservices"
+ icon = "GitHub"
+ type = "dashboard"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
diff --git a/internal/cli/models.go b/internal/cli/models.go
index 0d5f5dbc..9b1f0be5 100644
--- a/internal/cli/models.go
+++ b/internal/cli/models.go
@@ -222,6 +222,19 @@ type (
Approve ActionApprovePermissions `json:"approve"`
}
+ Page struct {
+ Meta
+ Identifier string `json:"identifier,omitempty"`
+ Type string `json:"type,omitempty"`
+ Icon *string `json:"icon,omitempty"`
+ Parent *string `json:"parent,omitempty"`
+ After *string `json:"after,omitempty"`
+ Title *string `json:"title,omitempty"`
+ Locked *bool `json:"locked,omitempty"`
+ Blueprint *string `json:"blueprint,omitempty"`
+ Widgets *[]map[string]any `json:"widgets,omitempty"`
+ }
+
Relation struct {
Identifier *string `json:"identifier,omitempty"`
Title *string `json:"title,omitempty"`
@@ -319,6 +332,7 @@ type PortBody struct {
Integration Webhook `json:"integration"`
Scorecard Scorecard `json:"Scorecard"`
Team Team `json:"team"`
+ Page Page `json:"page"`
MigrationId string `json:"migrationId"`
Migration Migration `json:"migration"`
}
diff --git a/internal/cli/page.go b/internal/cli/page.go
new file mode 100644
index 00000000..eb064110
--- /dev/null
+++ b/internal/cli/page.go
@@ -0,0 +1,91 @@
+package cli
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+)
+
+func (c *PortClient) GetPage(ctx context.Context, pageId string) (*Page, int, error) {
+ pb := &PortBody{}
+ url := "v1/pages/{page_identifier}"
+ resp, err := c.Client.R().
+ SetContext(ctx).
+ SetHeader("Accept", "application/json").
+ SetResult(pb).
+ SetPathParam("page_identifier", pageId).
+ Get(url)
+ if err != nil {
+ return nil, resp.StatusCode(), err
+ }
+ if !pb.OK {
+ return nil, resp.StatusCode(), fmt.Errorf("failed to get page, got: %s", resp.Body())
+ }
+ return &pb.Page, resp.StatusCode(), nil
+
+}
+
+func (c *PortClient) CreatePage(ctx context.Context, page *Page) (*Page, error) {
+ url := "v1/pages"
+ resp, err := c.Client.R().
+ SetBody(page).
+ SetContext(ctx).
+ Post(url)
+ if err != nil {
+ return nil, err
+ }
+ var pb PortBody
+ err = json.Unmarshal(resp.Body(), &pb)
+ if err != nil {
+ return nil, err
+ }
+ if !pb.OK {
+
+ if resp.IsSuccess() {
+ return nil, nil
+ }
+ return nil, fmt.Errorf("failed to create page, got: %s", resp.Body())
+ }
+ return &pb.Page, nil
+}
+
+func (c *PortClient) UpdatePage(ctx context.Context, pageId string, page *Page) (*Page, error) {
+ url := "v1/pages/{page_identifier}"
+ resp, err := c.Client.R().
+ SetBody(page).
+ SetContext(ctx).
+ SetPathParam("page_identifier", pageId).
+ Put(url)
+ if err != nil {
+ return nil, err
+ }
+ var pb PortBody
+ err = json.Unmarshal(resp.Body(), &pb)
+ if err != nil {
+ return nil, err
+ }
+ if !pb.OK {
+ return nil, fmt.Errorf("failed to update page, got: %s", resp.Body())
+ }
+ return &pb.Page, nil
+}
+
+func (c *PortClient) DeletePage(ctx context.Context, pageId string) (int, error) {
+ url := "v1/pages/{page_identifier}"
+ resp, err := c.Client.R().
+ SetContext(ctx).
+ SetPathParam("page_identifier", pageId).
+ Delete(url)
+ if err != nil {
+ return resp.StatusCode(), err
+ }
+ var pb PortBody
+ err = json.Unmarshal(resp.Body(), &pb)
+ if err != nil {
+ return resp.StatusCode(), err
+ }
+ if !pb.OK {
+ return resp.StatusCode(), fmt.Errorf("failed to delete page, got: %s", resp.Body())
+ }
+ return resp.StatusCode(), nil
+}
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
index 6b01876f..f012d021 100644
--- a/internal/utils/utils.go
+++ b/internal/utils/utils.go
@@ -22,7 +22,7 @@ func GenID() string {
if err != nil {
panic(err)
}
- return fmt.Sprintf("t-%s", id[:18])
+ return fmt.Sprintf("t-%s", id[len(id)-18:])
}
func TerraformListToGoArray(ctx context.Context, list types.List, arrayType string) ([]interface{}, error) {
diff --git a/port/page/model.go b/port/page/model.go
new file mode 100644
index 00000000..ba42e75b
--- /dev/null
+++ b/port/page/model.go
@@ -0,0 +1,20 @@
+package page
+
+import "github.com/hashicorp/terraform-plugin-framework/types"
+
+type PageModel struct {
+ ID types.String `tfsdk:"id"`
+ Identifier types.String `tfsdk:"identifier"`
+ Title types.String `tfsdk:"title"`
+ Type types.String `tfsdk:"type"`
+ Parent types.String `tfsdk:"parent"`
+ After types.String `tfsdk:"after"`
+ Icon types.String `tfsdk:"icon"`
+ Locked types.Bool `tfsdk:"locked"`
+ Blueprint types.String `tfsdk:"blueprint"`
+ Widgets []types.String `tfsdk:"widgets"`
+ CreatedAt types.String `tfsdk:"created_at"`
+ CreatedBy types.String `tfsdk:"created_by"`
+ UpdatedAt types.String `tfsdk:"updated_at"`
+ UpdatedBy types.String `tfsdk:"updated_by"`
+}
diff --git a/port/page/pageToPortBody.go b/port/page/pageToPortBody.go
new file mode 100644
index 00000000..5c909792
--- /dev/null
+++ b/port/page/pageToPortBody.go
@@ -0,0 +1,44 @@
+package page
+
+import (
+ "encoding/json"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+)
+
+func PageToPortBody(pm *PageModel) (*cli.Page, error) {
+ pb := &cli.Page{
+ Identifier: pm.Identifier.ValueString(),
+ Type: pm.Type.ValueString(),
+ Icon: pm.Icon.ValueStringPointer(),
+ Title: pm.Title.ValueStringPointer(),
+ Locked: pm.Locked.ValueBoolPointer(),
+ Blueprint: pm.Blueprint.ValueStringPointer(),
+ Parent: pm.Parent.ValueStringPointer(),
+ After: pm.After.ValueStringPointer(),
+ }
+
+ widgets, err := widgetsToPortBody(pm.Widgets)
+ if err != nil {
+ return nil, err
+ }
+ pb.Widgets = widgets
+
+ return pb, nil
+}
+
+func widgetsToPortBody(widgets []types.String) (*[]map[string]any, error) {
+ if widgets == nil {
+ return nil, nil
+ }
+ widgetsBody := make([]map[string]any, len(widgets))
+ for i, w := range widgets {
+ var widgetObject map[string]any
+ if err := json.Unmarshal([]byte(w.ValueString()), &widgetObject); err != nil {
+ return nil, err
+ }
+ widgetsBody[i] = widgetObject
+ }
+
+ return &widgetsBody, nil
+}
diff --git a/port/page/refreshPageToState.go b/port/page/refreshPageToState.go
new file mode 100644
index 00000000..b1013c09
--- /dev/null
+++ b/port/page/refreshPageToState.go
@@ -0,0 +1,32 @@
+package page
+
+import (
+ "encoding/json"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+)
+
+func refreshPageToState(pm *PageModel, b *cli.Page) error {
+ pm.ID = types.StringValue(b.Identifier)
+ pm.Identifier = types.StringValue(b.Identifier)
+ pm.Type = types.StringValue(b.Type)
+ pm.Icon = types.StringPointerValue(b.Icon)
+ pm.Parent = types.StringPointerValue(b.Parent)
+ pm.After = types.StringPointerValue(b.After)
+ pm.Title = types.StringPointerValue(b.Title)
+ pm.Locked = types.BoolPointerValue(b.Locked)
+ pm.Blueprint = types.StringPointerValue(b.Blueprint)
+
+ pm.Widgets = make([]types.String, len(*b.Widgets))
+ if b.Widgets != nil {
+ // go over each widget and convert it to a string and store it in the widgets array
+ for i, widget := range *b.Widgets {
+ bWidget, err := json.Marshal(widget)
+ if err != nil {
+ return err
+ }
+ pm.Widgets[i] = types.StringValue(string(bWidget))
+ }
+ }
+ return nil
+}
diff --git a/port/page/resource.go b/port/page/resource.go
new file mode 100644
index 00000000..0b02f93e
--- /dev/null
+++ b/port/page/resource.go
@@ -0,0 +1,178 @@
+package page
+
+import (
+ "context"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+)
+
+var _ resource.Resource = &PageResource{}
+var _ resource.ResourceWithImportState = &PageResource{}
+
+func NewPageResource() resource.Resource {
+ return &PageResource{}
+}
+
+type PageResource struct {
+ portClient *cli.PortClient
+}
+
+func (r *PageResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_page"
+}
+
+func (r *PageResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ r.portClient = req.ProviderData.(*cli.PortClient)
+}
+
+func (r *PageResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resp.Diagnostics.Append(resp.State.SetAttribute(
+ ctx, path.Root("identifier"), req.ID,
+ )...)
+
+ resp.Diagnostics.Append(resp.State.SetAttribute(
+ ctx, path.Root("id"), req.ID,
+ )...)
+}
+
+func (r *PageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state *PageModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ p, statusCode, err := r.portClient.GetPage(ctx, state.Identifier.ValueString())
+
+ if err != nil {
+ if statusCode == 404 {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError("failed to get page", err.Error())
+ return
+ }
+
+ err = refreshPageToState(state, p)
+
+ if err != nil {
+ resp.Diagnostics.AddError("failed to write page fields to resource", err.Error())
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+
+}
+
+func (r *PageResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state *PageModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if state.Identifier.ValueString() == "$home" {
+ tflog.Debug(ctx, "$home page is not deletable, unregistering from state")
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ statusCode, err := r.portClient.DeletePage(ctx, state.Identifier.ValueString())
+ if err != nil {
+ if statusCode == 404 {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError("failed to delete page", err.Error())
+ return
+ }
+
+ resp.State.RemoveResource(ctx)
+}
+
+func (r *PageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var state *PageModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ page, err := PageToPortBody(state)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to convert page resource to body", err.Error())
+ return
+ }
+
+ p, err := r.portClient.CreatePage(ctx, page)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to create page", err.Error())
+ return
+ }
+ if p == nil {
+ // if page is nil and err is nil this means that the page got created but the response body was empty
+ // to be forward compatible we will query the page again
+ p, _, err = r.portClient.GetPage(ctx, state.Identifier.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("failed to get page", err.Error())
+ return
+ }
+ }
+
+ state.ID = types.StringValue(p.Identifier)
+ state.CreatedAt = types.StringValue(p.CreatedAt.String())
+ state.CreatedBy = types.StringValue(p.CreatedBy)
+ state.UpdatedAt = types.StringValue(p.UpdatedAt.String())
+ state.UpdatedBy = types.StringValue(p.UpdatedBy)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PageResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var state *PageModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ p, _, err := r.portClient.GetPage(ctx, state.Identifier.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("failed to get page", err.Error())
+ return
+ }
+
+ page, err := PageToPortBody(state)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to convert page resource to body", err.Error())
+ return
+ }
+
+ _, err = r.portClient.UpdatePage(ctx, p.Identifier, page)
+
+ if err != nil {
+ resp.Diagnostics.AddError("failed to update page", err.Error())
+ return
+ }
+
+ state.ID = types.StringValue(p.Identifier)
+ state.CreatedAt = types.StringValue(p.CreatedAt.String())
+ state.CreatedBy = types.StringValue(p.CreatedBy)
+ state.UpdatedAt = types.StringValue(p.UpdatedAt.String())
+ state.UpdatedBy = types.StringValue(p.UpdatedBy)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+
+}
diff --git a/port/page/resource_test.go b/port/page/resource_test.go
new file mode 100644
index 00000000..55c23723
--- /dev/null
+++ b/port/page/resource_test.go
@@ -0,0 +1,307 @@
+package page_test
+
+import (
+ "fmt"
+ "os"
+ "regexp"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/port-labs/terraform-provider-port-labs/internal/acctest"
+ "github.com/port-labs/terraform-provider-port-labs/internal/utils"
+)
+
+func testAccCreateBlueprintConfig(identifier string) string {
+ return fmt.Sprintf(`
+ resource "port_blueprint" "microservice" {
+ title = "TF test microservice"
+ icon = "Terraform"
+ identifier = "%s"
+ properties = {
+ string_props = {
+ "text" = {
+ type = "string"
+ title = "text"
+ }
+ }
+ }
+ }
+ `, identifier)
+}
+
+func TestAccPortPageResourceBasicBetaEnabled(t *testing.T) {
+ blueprintIdentifier := utils.GenID()
+ pageIdentifier := utils.GenID()
+ err := os.Setenv("PORT_BETA_FEATURES_ENABLED", "true")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var testAccPortPageResourceBasic = testAccCreateBlueprintConfig(blueprintIdentifier) + fmt.Sprintf(`
+
+resource "port_page" "microservice_blueprint_page" {
+ identifier = "%s"
+ title = "Microservices"
+ icon = "Microservice"
+ blueprint = port_blueprint.microservice.identifier
+ type = "blueprint-entities"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "blabla",
+ "type" : "table-entities-explorer",
+ "dataset" : {
+ "combinator" : "and",
+ "rules" : [
+ {
+ "operator" : "=",
+ "property" : "$blueprint",
+ "value" : "{{blueprint}}"
+ }
+ ]
+ }
+ }
+ )
+ ]
+}
+`, pageIdentifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccPortPageResourceBasic,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_page.microservice_blueprint_page", "identifier", pageIdentifier),
+ resource.TestCheckResourceAttr("port_page.microservice_blueprint_page", "title", "Microservices"),
+ resource.TestCheckResourceAttr("port_page.microservice_blueprint_page", "icon", "Microservice"),
+ resource.TestCheckResourceAttr("port_page.microservice_blueprint_page", "blueprint", blueprintIdentifier),
+ resource.TestCheckResourceAttr("port_page.microservice_blueprint_page", "type", "blueprint-entities"),
+ resource.TestCheckResourceAttr("port_page.microservice_blueprint_page", "widgets.#", "1"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortPageResourceBasicBetaDisabled(t *testing.T) {
+ blueprintIdentifier := utils.GenID()
+ pageIdentifier := utils.GenID()
+ err := os.Setenv("PORT_BETA_FEATURES_ENABLED", "false")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var testAccPortPageResourceBasic = testAccCreateBlueprintConfig(blueprintIdentifier) + fmt.Sprintf(`
+
+resource "port_page" "microservice_blueprint_page" {
+ identifier = "%s"
+ title = "Microservices"
+ icon = "Microservice"
+ blueprint = port_blueprint.microservice.identifier
+ type = "blueprint-entities"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "blabla",
+ "type" : "table-entities-explorer",
+ "dataset" : {
+ "combinator" : "and",
+ "rules" : [
+ {
+ "operator" : "=",
+ "property" : "$blueprint",
+ "value" : "{{blueprint}}"
+ }
+ ]
+ }
+ }
+ )
+ ]
+}
+`, pageIdentifier)
+
+ // expect to fail on beta feature not enabled
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccPortPageResourceBasic,
+ ExpectError: regexp.MustCompile("Beta features are not enabled"),
+ },
+ },
+ })
+}
+
+func TestAccPortPageResourceCreateDashboardPage(t *testing.T) {
+ pageIdentifier := utils.GenID()
+ err := os.Setenv("PORT_BETA_FEATURES_ENABLED", "true")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var testAccPortPageResourceBasic = fmt.Sprintf(`
+
+resource "port_page" "microservice_dashboard_page" {
+ identifier = "%s"
+ title = "dashboards"
+ icon = "GitHub"
+ type = "dashboard"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
+`, pageIdentifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccPortPageResourceBasic,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "identifier", pageIdentifier),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "title", "dashboards"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "icon", "GitHub"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "type", "dashboard"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "widgets.#", "1"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortPageResourceCreatePageAfterPage(t *testing.T) {
+ pageIdentifier := utils.GenID()
+ err := os.Setenv("PORT_BETA_FEATURES_ENABLED", "true")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var testAccPortPageResourceBasic = fmt.Sprintf(`
+
+resource "port_page" "microservice_dashboard_page" {
+ identifier = "%s"
+ title = "dashboards"
+ icon = "GitHub"
+ type = "dashboard"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
+`, pageIdentifier)
+
+ pageIdentifier2 := utils.GenID()
+ var testAccPortPageResourceBasic2 = fmt.Sprintf(`
+
+resource "port_page" "microservice_dashboard_page_2" {
+ identifier = "%s"
+ title = "Microservices_2"
+ icon = "GitHub"
+ after = port_page.microservice_dashboard_page.identifier
+ type = "dashboard"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
+`, pageIdentifier2)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccPortPageResourceBasic + testAccPortPageResourceBasic2,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "identifier", pageIdentifier),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "title", "dashboards"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "icon", "GitHub"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "type", "dashboard"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "widgets.#", "1"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page_2", "identifier", pageIdentifier2),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page_2", "title", "Microservices_2"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page_2", "after", pageIdentifier),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page_2", "icon", "GitHub"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page_2", "type", "dashboard"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page_2", "widgets.#", "1"),
+ ),
+ },
+ },
+ })
+}
diff --git a/port/page/schema.go b/port/page/schema.go
new file mode 100644
index 00000000..795a7a24
--- /dev/null
+++ b/port/page/schema.go
@@ -0,0 +1,351 @@
+package page
+
+import (
+ "context"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "os"
+)
+
+func PageSchema() map[string]schema.Attribute {
+ return map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "identifier": schema.StringAttribute{
+ Description: "The Identifier of the page",
+ Required: true,
+ },
+ "type": schema.StringAttribute{
+ Description: "The type of the page, can be one of \"blueprint-entities\", \"dashboard\" or \"home\"",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf(
+ "blueprint-entities",
+ "dashboard",
+ "home",
+ ),
+ },
+ },
+ "parent": schema.StringAttribute{
+ Description: "The identifier of the folder in which the page is in, default is the root of the sidebar",
+ Optional: true,
+ },
+ "after": schema.StringAttribute{
+ Description: "The identifier of the page/folder after which the page should be placed",
+ Optional: true,
+ },
+ "icon": schema.StringAttribute{
+ Description: "The icon of the page",
+ Optional: true,
+ },
+ "title": schema.StringAttribute{
+ Description: "The title of the page",
+ Optional: true,
+ },
+ "locked": schema.BoolAttribute{
+ Description: "Whether the page is locked, if true, viewers will not be able to edit the page widgets and filters",
+ Optional: true,
+ },
+ "blueprint": schema.StringAttribute{
+ Description: "The blueprint for which the page is created, relevant only for pages of type \"blueprint-entities\"",
+ Optional: true,
+ },
+ "widgets": schema.ListAttribute{
+ Description: "The widgets of the page",
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ "created_at": schema.StringAttribute{
+ MarkdownDescription: "The creation date of the page",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "created_by": schema.StringAttribute{
+ MarkdownDescription: "The creator of the page",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "updated_at": schema.StringAttribute{
+ MarkdownDescription: "The last update date of the page",
+ Computed: true,
+ },
+ "updated_by": schema.StringAttribute{
+ MarkdownDescription: "The last updater of the page",
+ Computed: true,
+ },
+ }
+}
+
+func (r *PageResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: PageResourceMarkdownDescription,
+ Attributes: PageSchema(),
+ }
+}
+
+func (r *PageResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
+ var state PageModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ betaFeaturesEnabledEnv := os.Getenv("PORT_BETA_FEATURES_ENABLED")
+ if !(betaFeaturesEnabledEnv == "true") {
+ resp.Diagnostics.AddError("Beta features are not enabled", "Page resource is currently in beta and is subject to change in future versions. Use it by setting the Environment Variable PORT_BETA_FEATURES_ENABLED=true.")
+ return
+ }
+}
+
+var PageResourceMarkdownDescription = `
+
+# Page resource
+
+Docs about the different page types can be found [here](https://docs.getport.io/customize-pages-dashboards-and-plugins/page/catalog-page).
+
+~> **WARNING**
+The page resource is currently in beta and is subject to change in future versions.
+Use it by setting the Environment Variable ` + "`PORT_BETA_FEATURES_ENABLED=true`" + `.
+If this Environment Variable isn't specified, you won't be able to use the resource.
+
+## Example Usage
+
+### Blueprint Entities Page
+
+` + "```hcl" + `
+
+resource "port_page" "microservice_blueprint_page" {
+ identifier = "microservice_blueprint_page"
+ title = "Microservices"
+ type = "blueprint-entities"
+ icon = "Microservice"
+ blueprint = port_blueprint.base_blueprint.identifier
+ widgets = [
+ jsonencode(
+ {
+ "id" : "microservice-table-entities",
+ "type" : "table-entities-explorer",
+ "dataset" : {
+ "combinator" : "and",
+ "rules" : [
+ {
+ "operator" : "=",
+ "property" : "$blueprint",
+ "value" : ` + "{{`\"{{blueprint}}\"`}}" + `
+ }
+ ]
+ }
+ }
+ )
+ ]
+}
+
+` + "```" + `
+
+### Dashboard Page
+
+` + "```hcl" + `
+
+resource "port_page" "microservice_dashboard_page" {
+ identifier = "microservice_dashboard_page"
+ title = "Microservices"
+ icon = "GitHub"
+ type = "dashboard"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
+
+` + "```" + `
+
+
+### Page with parent
+
+Create a page inside a folder.
+
+` + "```hcl" + `
+
+resource "port_page" "microservice_dashboard_page" {
+ identifier = "microservice_dashboard_page"
+ title = "Microservices"
+ icon = "GitHub"
+ type = "dashboard"
+ parent = "microservices-folder"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
+
+` + "```" + `
+
+
+### Page with after
+
+Create a page after another page.
+
+` + "```hcl" + `
+
+resource "port_page" "microservice_dashboard_page" {
+ identifier = "microservice_dashboard_page"
+ title = "Microservices"
+ icon = "GitHub"
+ type = "dashboard"
+ after = "microservices_entities_page"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
+
+` + "```" + `
+
+### Home Page
+
+` + "```hcl" + `
+
+resource "port_page" "home_page" {
+ identifier = "$home"
+ title = "Home"
+ type = "home"
+ widgets = [
+ jsonencode(
+ {
+ "type" : "dashboard-widget",
+ "id" : "azkLJD6wLk6nJSvA",
+ "layout" : [
+ {
+ "columns" : [
+ {
+ "id" : "markdown",
+ "size" : 6
+ },
+ {
+ "id" : "overview",
+ "size" : 6
+ },
+ ],
+ "height" : 648
+ }
+ ],
+ "widgets" : [
+ {
+ "type" : "markdown",
+ "markdown" : "## Welcome to your internal developer portal",
+ "icon" : "port",
+ "title" : "About developer Portal",
+ "id" : "markdown"
+ },
+ {
+ "type" : "iframe-widget",
+ "id" : "overview",
+ "title" : "Overview",
+ "icon" : "Docs",
+ "url" : "https://www.youtube.com/embed/ggXL2ZsPVQM?si=xj6XtV0faatoOhss",
+ "urlType" : "public"
+ }
+ ]
+ }
+ )
+ ]
+}
+
+` + "```" + `
+
+The home page is a special page, which is created by default when you create a new organization.
+
+- When deleting the home page resource using terraform, the home page will not be deleted from Port as it isn't deletable page, instead, the home page will be removed from the terraform state.
+- Due to only having one home page you'll have to import the state of the home page manually.
+
+` + "```" + `
+terraform import port_page.home_page "\$home"
+` + "```" + `
+
+`
diff --git a/port/webhook/resource_test.go b/port/webhook/resource_test.go
index d306c002..a8dc2e06 100644
--- a/port/webhook/resource_test.go
+++ b/port/webhook/resource_test.go
@@ -82,22 +82,22 @@ func TestAccPortWebhook(t *testing.T) {
icon = "Terraform"
enabled = true
security = {
- secret = "test"
+ //secret = "test"
signature_header_name = "X-Hub-Signature-256"
signature_algorithm = "sha256"
signature_prefix = "sha256="
- request_identifier_path = "body.repository.full_name"
+ request_identifier_path = ".body.repository.full_name"
}
mappings = [
{
"blueprint" = "%s",
"filter" = ".headers.\"X-GitHub-Event\" == \"pull_request\"",
- "items_to_parse" = "body.pull_request",
+ "items_to_parse" = ".body.pull_request",
"entity" = {
"identifier" = ".body.pull_request.id | tostring",
"title" = ".body.pull_request.title",
- "icon" = "Terraform",
- "team" = "port",
+ "icon" = "\"Terraform\"",
+ "team" = "\"port\"",
"properties" = {
"author" = ".body.pull_request.user.login",
"url" = ".body.pull_request.html_url"
@@ -107,7 +107,6 @@ func TestAccPortWebhook(t *testing.T) {
]
lifecycle {
ignore_changes = [
- security.secret
]
}
depends_on = [
@@ -143,14 +142,14 @@ func TestAccPortWebhook(t *testing.T) {
resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_header_name", "X-Hub-Signature-256"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_algorithm", "sha256"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_prefix", "sha256="),
- resource.TestCheckResourceAttr("port_webhook.create_pr", "security.request_identifier_path", "body.repository.full_name"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "security.request_identifier_path", ".body.repository.full_name"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.blueprint", identifier),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.filter", ".headers.\"X-GitHub-Event\" == \"pull_request\""),
- resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.items_to_parse", "body.pull_request"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.items_to_parse", ".body.pull_request"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.identifier", ".body.pull_request.id | tostring"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.title", ".body.pull_request.title"),
- resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.icon", "Terraform"),
- resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.team", "port"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.icon", "\"Terraform\""),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.team", "\"port\""),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.properties.author", ".body.pull_request.user.login"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.properties.url", ".body.pull_request.html_url"),
),
@@ -172,7 +171,7 @@ func TestAccPortWebhookImport(t *testing.T) {
signature_header_name = "X-Hub-Signature-256"
signature_algorithm = "sha256"
signature_prefix = "sha256="
- request_identifier_path = "body.repository.full_name"
+ request_identifier_path = ".body.repository.full_name"
}
mappings = [
{
@@ -181,8 +180,8 @@ func TestAccPortWebhookImport(t *testing.T) {
"entity" = {
"identifier" = ".body.pull_request.id | tostring",
"title" = ".body.pull_request.title",
- "icon" = "Terraform",
- "team" = "port",
+ "icon" = "\"Terraform\"",
+ "team" = "\"port\"",
"properties" = {
"author" = ".body.pull_request.user.login",
"url" = ".body.pull_request.html_url"
@@ -192,7 +191,6 @@ func TestAccPortWebhookImport(t *testing.T) {
]
lifecycle {
ignore_changes = [
- security.secret
]
}
depends_on = [
@@ -228,13 +226,13 @@ func TestAccPortWebhookImport(t *testing.T) {
resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_header_name", "X-Hub-Signature-256"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_algorithm", "sha256"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_prefix", "sha256="),
- resource.TestCheckResourceAttr("port_webhook.create_pr", "security.request_identifier_path", "body.repository.full_name"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "security.request_identifier_path", ".body.repository.full_name"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.blueprint", identifier),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.filter", ".headers.\"X-GitHub-Event\" == \"pull_request\""),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.identifier", ".body.pull_request.id | tostring"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.title", ".body.pull_request.title"),
- resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.icon", "Terraform"),
- resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.team", "port"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.icon", "\"Terraform\""),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.team", "\"port\""),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.properties.author", ".body.pull_request.user.login"),
resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.properties.url", ".body.pull_request.html_url"),
),
diff --git a/provider/provider.go b/provider/provider.go
index 099c3135..c5204f68 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -2,6 +2,7 @@ package provider
import (
"context"
+ "github.com/port-labs/terraform-provider-port-labs/port/page"
"os"
"github.com/hashicorp/terraform-plugin-framework/datasource"
@@ -134,6 +135,7 @@ func (p *PortLabsProvider) Resources(ctx context.Context) []func() resource.Reso
webhook.NewWebhookResource,
scorecard.NewScorecardResource,
team.NewTeamResource,
+ page.NewPageResource,
}
}
From 8bfdf4d7494526286b367265cbd9cfdbc9853fa5 Mon Sep 17 00:00:00 2001
From: Tom Tankilevitch <59158507+Tankilevitch@users.noreply.github.com>
Date: Tue, 20 Feb 2024 20:52:02 +0200
Subject: [PATCH 04/25] Add page-permissions resource (#107)
---
docs/resources/port_page_permissions.md | 123 +++++++++++++++
go.mod | 1 +
go.sum | 2 +
internal/cli/models.go | 15 ++
internal/cli/pagePermissions.go | 48 ++++++
port/page-permissions/model.go | 15 ++
.../pagePermissionToPortBody.go | 21 +++
.../refreshPagePermissionsToState.go | 29 ++++
port/page-permissions/resource.go | 145 +++++++++++++++++
port/page-permissions/resource_test.go | 149 ++++++++++++++++++
port/page-permissions/schema.go | 98 ++++++++++++
provider/provider.go | 7 +-
12 files changed, 650 insertions(+), 3 deletions(-)
create mode 100644 docs/resources/port_page_permissions.md
create mode 100644 internal/cli/pagePermissions.go
create mode 100644 port/page-permissions/model.go
create mode 100644 port/page-permissions/pagePermissionToPortBody.go
create mode 100644 port/page-permissions/refreshPagePermissionsToState.go
create mode 100644 port/page-permissions/resource.go
create mode 100644 port/page-permissions/resource_test.go
create mode 100644 port/page-permissions/schema.go
diff --git a/docs/resources/port_page_permissions.md b/docs/resources/port_page_permissions.md
new file mode 100644
index 00000000..68a62550
--- /dev/null
+++ b/docs/resources/port_page_permissions.md
@@ -0,0 +1,123 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "port_page_permissions Resource - terraform-provider-port-labs"
+subcategory: ""
+description: |-
+ Page Permissions resource
+ Docs about page permissions can be found here https://docs.getport.io/customize-pages-dashboards-and-plugins/page/page-permissions?view-permissions=api.
+ Example Usage
+ Allow read access to all members:
+ hcl
+ resource "port_page_permissions" "microservices_permissions" {
+ page_identifier = "microservices"
+ read = {
+ "roles": ["Member"],
+ "users": [],
+ "teams": [],
+ }
+ }
+
+ Allow read access to all admins and a specific user and team:
+ hcl
+ resource "port_page_permissions" "microservices_permissions" {
+ page_identifier = "microservices"
+ read = {
+ "roles": [
+ "Admin",
+ ],
+ "users": ["test-admin-user@test.com"],
+ "teams": ["Team Spiderman"],
+ }
+ }
+
+ Allow read access to specific users and teams:
+ hcl
+ resource "port_page_permissions" "microservices_permissions" {
+ page_identifier = "microservices"
+ read = {
+ "roles": [],
+ "users": ["test-admin-user@test.com"],
+ "teams": ["Team Spiderman"],
+ }
+ }
+
+ Disclaimer
+ Page permissions are created by default when page is first created, this means that you should use this resource when you want to change the default permissions of a page.When deleting a page permissions resource using terraform, the page permissions will not be deleted from Port, as they are required for the action to work, instead, the page permissions will be removed from the terraform state.
+---
+
+# port_page_permissions (Resource)
+
+# Page Permissions resource
+
+Docs about page permissions can be found [here](https://docs.getport.io/customize-pages-dashboards-and-plugins/page/page-permissions?view-permissions=api).
+
+## Example Usage
+
+### Allow read access to all members:
+
+```hcl
+resource "port_page_permissions" "microservices_permissions" {
+ page_identifier = "microservices"
+ read = {
+ "roles": ["Member"],
+ "users": [],
+ "teams": [],
+ }
+}
+```
+
+### Allow read access to all admins and a specific user and team:
+
+```hcl
+resource "port_page_permissions" "microservices_permissions" {
+ page_identifier = "microservices"
+ read = {
+ "roles": [
+ "Admin",
+ ],
+ "users": ["test-admin-user@test.com"],
+ "teams": ["Team Spiderman"],
+ }
+}
+```
+
+### Allow read access to specific users and teams:
+
+```hcl
+resource "port_page_permissions" "microservices_permissions" {
+ page_identifier = "microservices"
+ read = {
+ "roles": [],
+ "users": ["test-admin-user@test.com"],
+ "teams": ["Team Spiderman"],
+ }
+}
+```
+
+## Disclaimer
+
+- Page permissions are created by default when page is first created, this means that you should use this resource when you want to change the default permissions of a page.
+- When deleting a page permissions resource using terraform, the page permissions will not be deleted from Port, as they are required for the action to work, instead, the page permissions will be removed from the terraform state.
+
+
+
+
+## Schema
+
+### Required
+
+- `page_identifier` (String)
+- `read` (Attributes) The permission to read the page (see [below for nested schema](#nestedatt--read))
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `read`
+
+Optional:
+
+- `roles` (List of String) The roles with read permission
+- `teams` (List of String) The teams with read permission
+- `users` (List of String) The users with read permission
diff --git a/go.mod b/go.mod
index 217f1e4a..e4335c47 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
require (
github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec // indirect
github.com/cloudflare/circl v1.3.3 // indirect
+ github.com/gertd/go-pluralize v0.2.1 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.27.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
diff --git a/go.sum b/go.sum
index 963a625a..733b534e 100644
--- a/go.sum
+++ b/go.sum
@@ -33,6 +33,8 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
+github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=
+github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
diff --git a/internal/cli/models.go b/internal/cli/models.go
index 9b1f0be5..9293af4b 100644
--- a/internal/cli/models.go
+++ b/internal/cli/models.go
@@ -235,6 +235,16 @@ type (
Widgets *[]map[string]any `json:"widgets,omitempty"`
}
+ PageReadPermissions struct {
+ Users []string `json:"users"`
+ Roles []string `json:"roles"`
+ Teams []string `json:"teams"`
+ }
+
+ PagePermissions struct {
+ Read PageReadPermissions `json:"read"`
+ }
+
Relation struct {
Identifier *string `json:"identifier,omitempty"`
Title *string `json:"title,omitempty"`
@@ -337,6 +347,11 @@ type PortBody struct {
Migration Migration `json:"migration"`
}
+type PortPagePermissionsBody struct {
+ OK bool `json:"ok"`
+ PagePermissions PagePermissions `json:"permissions"`
+}
+
type TeamUserBody struct {
Email string `json:"email"`
}
diff --git a/internal/cli/pagePermissions.go b/internal/cli/pagePermissions.go
new file mode 100644
index 00000000..73cfe93c
--- /dev/null
+++ b/internal/cli/pagePermissions.go
@@ -0,0 +1,48 @@
+package cli
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+)
+
+func (c *PortClient) GetPagePermissions(ctx context.Context, pageID string) (*PagePermissions, int, error) {
+ pppb := &PortPagePermissionsBody{}
+ url := "v1/pages/{page_identifier}/permissions"
+ resp, err := c.Client.R().
+ SetContext(ctx).
+ SetHeader("Accept", "application/json").
+ SetResult(pppb).
+ SetPathParam("page_identifier", pageID).
+ Get(url)
+ if err != nil {
+ return nil, resp.StatusCode(), err
+ }
+ if !pppb.OK {
+ return nil, resp.StatusCode(), fmt.Errorf("failed to get page permissions, got: %s", resp.Body())
+ }
+ return &pppb.PagePermissions, resp.StatusCode(), nil
+
+}
+
+func (c *PortClient) UpdatePagePermissions(ctx context.Context, pageID string, permissions *PagePermissions) (*PagePermissions, error) {
+ url := "v1/pages/{page_identifier}/permissions"
+
+ resp, err := c.Client.R().
+ SetBody(permissions).
+ SetContext(ctx).
+ SetPathParam("page_identifier", pageID).
+ Patch(url)
+ if err != nil {
+ return nil, err
+ }
+ var pppb PortPagePermissionsBody
+ err = json.Unmarshal(resp.Body(), &pppb)
+ if err != nil {
+ return nil, err
+ }
+ if !pppb.OK {
+ return nil, fmt.Errorf("failed to update page permissions, got: %s", resp.Body())
+ }
+ return &pppb.PagePermissions, nil
+}
diff --git a/port/page-permissions/model.go b/port/page-permissions/model.go
new file mode 100644
index 00000000..e9a0fc10
--- /dev/null
+++ b/port/page-permissions/model.go
@@ -0,0 +1,15 @@
+package page_permissions
+
+import "github.com/hashicorp/terraform-plugin-framework/types"
+
+type ReadPagePermissionsModel struct {
+ Users []types.String `tfsdk:"users"`
+ Roles []types.String `tfsdk:"roles"`
+ Teams []types.String `tfsdk:"teams"`
+}
+
+type PagePermissionsModel struct {
+ ID types.String `tfsdk:"id"`
+ PageIdentifier types.String `tfsdk:"page_identifier"`
+ Read ReadPagePermissionsModel `tfsdk:"read"`
+}
diff --git a/port/page-permissions/pagePermissionToPortBody.go b/port/page-permissions/pagePermissionToPortBody.go
new file mode 100644
index 00000000..e7799f42
--- /dev/null
+++ b/port/page-permissions/pagePermissionToPortBody.go
@@ -0,0 +1,21 @@
+package page_permissions
+
+import (
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+ "github.com/port-labs/terraform-provider-port-labs/internal/flex"
+)
+
+func pagePermissionsToPortBody(state *PagePermissionsModel) (*cli.PagePermissions, error) {
+ if state == nil {
+ return nil, nil
+ }
+
+ pagePermissions := cli.PagePermissions{
+ Read: cli.PageReadPermissions{
+ Users: flex.TerraformStringListToGoArray(state.Read.Users),
+ Roles: flex.TerraformStringListToGoArray(state.Read.Roles),
+ Teams: flex.TerraformStringListToGoArray(state.Read.Teams),
+ },
+ }
+ return &pagePermissions, nil
+}
diff --git a/port/page-permissions/refreshPagePermissionsToState.go b/port/page-permissions/refreshPagePermissionsToState.go
new file mode 100644
index 00000000..c26971b7
--- /dev/null
+++ b/port/page-permissions/refreshPagePermissionsToState.go
@@ -0,0 +1,29 @@
+package page_permissions
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+)
+
+func refreshPagePermissionsState(state *PagePermissionsModel, a *cli.PagePermissions, pageId string) error {
+ state.ID = types.StringValue(pageId)
+ state.PageIdentifier = types.StringValue(pageId)
+ state.Read = ReadPagePermissionsModel{}
+
+ state.Read.Users = make([]types.String, len(a.Read.Users))
+ for i, u := range a.Read.Users {
+ state.Read.Users[i] = types.StringValue(u)
+ }
+
+ state.Read.Roles = make([]types.String, len(a.Read.Roles))
+ for i, u := range a.Read.Roles {
+ state.Read.Roles[i] = types.StringValue(u)
+ }
+
+ state.Read.Teams = make([]types.String, len(a.Read.Teams))
+ for i, u := range a.Read.Teams {
+ state.Read.Teams[i] = types.StringValue(u)
+ }
+
+ return nil
+}
diff --git a/port/page-permissions/resource.go b/port/page-permissions/resource.go
new file mode 100644
index 00000000..a44fc7f9
--- /dev/null
+++ b/port/page-permissions/resource.go
@@ -0,0 +1,145 @@
+package page_permissions
+
+import (
+ "context"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+)
+
+var _ resource.Resource = &PagePermissionsResource{}
+var _ resource.ResourceWithImportState = &PagePermissionsResource{}
+
+func NewPagePermissionsResource() resource.Resource {
+ return &PagePermissionsResource{}
+}
+
+type PagePermissionsResource struct {
+ portClient *cli.PortClient
+}
+
+func (r *PagePermissionsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_page_permissions"
+}
+
+func (r *PagePermissionsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ r.portClient = req.ProviderData.(*cli.PortClient)
+}
+
+func (r *PagePermissionsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resp.Diagnostics.Append(resp.State.SetAttribute(
+ ctx, path.Root("page_identifier"), req.ID,
+ )...)
+
+ resp.Diagnostics.Append(resp.State.SetAttribute(
+ ctx, path.Root("id"), req.ID,
+ )...)
+}
+
+func (r *PagePermissionsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state *PagePermissionsModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ pageIdentifier := state.PageIdentifier.ValueString()
+
+ a, statusCode, err := r.portClient.GetPagePermissions(ctx, pageIdentifier)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to read page permissions", err.Error())
+ return
+ }
+
+ if statusCode == 404 {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ err = refreshPagePermissionsState(state, a, pageIdentifier)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to refresh page permissions state", err.Error())
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+
+}
+
+func (r *PagePermissionsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var state *PagePermissionsModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ pageIdentifier := state.PageIdentifier.ValueString()
+
+ pagePermissions, err := pagePermissionsToPortBody(state)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to convert page permissions to port body", err.Error())
+ return
+ }
+
+ _, err = r.portClient.UpdatePagePermissions(ctx, pageIdentifier, pagePermissions)
+
+ if err != nil {
+ resp.Diagnostics.AddError("failed to update page permissions", err.Error())
+ return
+ }
+
+ state.ID = types.StringValue(pageIdentifier)
+ state.PageIdentifier = types.StringValue(pageIdentifier)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *PagePermissionsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state *PagePermissionsModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // pagePermissions is not deletable resource by itself as it is tied to an page and is created by default when an page is created
+ resp.State.RemoveResource(ctx)
+}
+
+func (r *PagePermissionsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var state *PagePermissionsModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ pageIdentifier := state.PageIdentifier.ValueString()
+
+ pagePermissions, err := pagePermissionsToPortBody(state)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to convert page permissions to port body", err.Error())
+ return
+ }
+
+ _, err = r.portClient.UpdatePagePermissions(ctx, pageIdentifier, pagePermissions)
+
+ if err != nil {
+ resp.Diagnostics.AddError("failed to update page permissions", err.Error())
+ return
+ }
+
+ state.ID = types.StringValue(pageIdentifier)
+ state.PageIdentifier = types.StringValue(pageIdentifier)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+
+}
diff --git a/port/page-permissions/resource_test.go b/port/page-permissions/resource_test.go
new file mode 100644
index 00000000..3a9d96cf
--- /dev/null
+++ b/port/page-permissions/resource_test.go
@@ -0,0 +1,149 @@
+package page_permissions_test
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/port-labs/terraform-provider-port-labs/internal/acctest"
+ "github.com/port-labs/terraform-provider-port-labs/internal/utils"
+)
+
+func createPage(identifier string) string {
+ return fmt.Sprintf(`
+
+resource "port_page" "microservice_dashboard_page" {
+ identifier = "%s"
+ title = "dashboards"
+ icon = "GitHub"
+ type = "dashboard"
+ widgets = [
+ jsonencode(
+ {
+ "id" : "dashboardWidget",
+ "layout" : [
+ {
+ "height" : 400,
+ "columns" : [
+ {
+ "id" : "microserviceGuide",
+ "size" : 12
+ }
+ ]
+ }
+ ],
+ "type" : "dashboard-widget",
+ "widgets" : [
+ {
+ "title" : "Microservices Guide",
+ "icon" : "BlankPage",
+ "markdown" : "# This is the new Microservice Dashboard",
+ "type" : "markdown",
+ "description" : "",
+ "id" : "microserviceGuide"
+ }
+ ],
+ }
+ )
+ ]
+}
+`, identifier)
+}
+
+func TestAccPortPagePermissionsBasic(t *testing.T) {
+ pageIdentifier := utils.GenID()
+ err := os.Setenv("PORT_BETA_FEATURES_ENABLED", "true")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var testAccPortPageResourceBasic = createPage(pageIdentifier)
+
+ var testAccBasePagePermissionsConfigUpdate = `
+
+ resource "port_page_permissions" "microservice_permissions" {
+ page_identifier = port_page.microservice_dashboard_page.identifier
+ read = {
+ "roles": [
+ "Member",
+ ],
+ "users": [],
+ "teams": []
+ }
+ }`
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccPortPageResourceBasic + testAccBasePagePermissionsConfigUpdate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "identifier", pageIdentifier),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "title", "dashboards"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "icon", "GitHub"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "type", "dashboard"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "widgets.#", "1"),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "page_identifier", pageIdentifier),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "read.roles.#", "1"),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "read.roles.0", "Member"),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "read.users.#", "0"),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "read.teams.#", "0"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortPagePermissionsUpdateWithUsers(t *testing.T) {
+ pageIdentifier := utils.GenID()
+ err := os.Setenv("PORT_BETA_FEATURES_ENABLED", "true")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var testAccPortPageResourceBasic = createPage(pageIdentifier)
+
+ teamName := utils.GenID()
+
+ var testAccBasePagePermissionsConfigUpdate = fmt.Sprintf(`
+
+ resource "port_team" "team" {
+ name = "%s"
+ description = "Test description"
+ users = []
+ }
+
+ resource "port_page_permissions" "microservice_permissions" {
+ page_identifier = port_page.microservice_dashboard_page.identifier
+ read = {
+ "roles": [
+ "Member",
+ ],
+ "users": [],
+ "teams": [port_team.team.name],
+ }
+ }`, teamName)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccPortPageResourceBasic + testAccBasePagePermissionsConfigUpdate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "identifier", pageIdentifier),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "title", "dashboards"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "icon", "GitHub"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "type", "dashboard"),
+ resource.TestCheckResourceAttr("port_page.microservice_dashboard_page", "widgets.#", "1"),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "page_identifier", pageIdentifier),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "read.roles.#", "1"),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "read.roles.0", "Member"),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "read.users.#", "0"),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "read.teams.#", "1"),
+ resource.TestCheckResourceAttr("port_page_permissions.microservice_permissions", "read.teams.0", teamName),
+ ),
+ },
+ },
+ })
+}
diff --git a/port/page-permissions/schema.go b/port/page-permissions/schema.go
new file mode 100644
index 00000000..d9c999c5
--- /dev/null
+++ b/port/page-permissions/schema.go
@@ -0,0 +1,98 @@
+package page_permissions
+
+import (
+ "context"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+func PagePermissionsSchema() map[string]schema.Attribute {
+ return map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "page_identifier": schema.StringAttribute{
+ Required: true,
+ },
+ "read": schema.SingleNestedAttribute{
+ MarkdownDescription: "The permission to read the page",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "users": schema.ListAttribute{
+ MarkdownDescription: "The users with read permission",
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ "roles": schema.ListAttribute{
+ MarkdownDescription: "The roles with read permission",
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ "teams": schema.ListAttribute{
+ MarkdownDescription: "The teams with read permission",
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ },
+ }}
+}
+
+func (r *PagePermissionsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: PagePermissionsResourceMarkdownDescription,
+ Attributes: PagePermissionsSchema(),
+ }
+}
+
+var PagePermissionsResourceMarkdownDescription = `
+
+# Page Permissions resource
+
+Docs about page permissions can be found [here](https://docs.getport.io/customize-pages-dashboards-and-plugins/page/page-permissions?view-permissions=api).
+
+## Example Usage
+
+### Allow read access to all members:
+
+` + "```hcl" + `
+resource "port_page_permissions" "microservices_permissions" {
+ page_identifier = "microservices"
+ read = {
+ "roles": ["Member"],
+ "users": [],
+ "teams": [],
+ }
+}` + "\n```" + `
+
+### Allow read access to all admins and a specific user and team:
+
+` + "```hcl" + `
+resource "port_page_permissions" "microservices_permissions" {
+ page_identifier = "microservices"
+ read = {
+ "roles": [
+ "Admin",
+ ],
+ "users": ["test-admin-user@test.com"],
+ "teams": ["Team Spiderman"],
+ }
+}` + "\n```" + `
+
+### Allow read access to specific users and teams:
+
+` + "```hcl" + `
+resource "port_page_permissions" "microservices_permissions" {
+ page_identifier = "microservices"
+ read = {
+ "roles": [],
+ "users": ["test-admin-user@test.com"],
+ "teams": ["Team Spiderman"],
+ }
+}` + "\n```" + `
+
+## Disclaimer
+
+- Page permissions are created by default when page is first created, this means that you should use this resource when you want to change the default permissions of a page.
+- When deleting a page permissions resource using terraform, the page permissions will not be deleted from Port, as they are required for the action to work, instead, the page permissions will be removed from the terraform state.
+`
diff --git a/provider/provider.go b/provider/provider.go
index c5204f68..911f10f3 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -2,9 +2,6 @@ package provider
import (
"context"
- "github.com/port-labs/terraform-provider-port-labs/port/page"
- "os"
-
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
@@ -16,10 +13,13 @@ import (
"github.com/port-labs/terraform-provider-port-labs/port/aggregation-properties"
"github.com/port-labs/terraform-provider-port-labs/port/blueprint"
"github.com/port-labs/terraform-provider-port-labs/port/entity"
+ "github.com/port-labs/terraform-provider-port-labs/port/page"
+ "github.com/port-labs/terraform-provider-port-labs/port/page-permissions"
"github.com/port-labs/terraform-provider-port-labs/port/scorecard"
"github.com/port-labs/terraform-provider-port-labs/port/team"
"github.com/port-labs/terraform-provider-port-labs/port/webhook"
"github.com/port-labs/terraform-provider-port-labs/version"
+ "os"
)
var (
@@ -136,6 +136,7 @@ func (p *PortLabsProvider) Resources(ctx context.Context) []func() resource.Reso
scorecard.NewScorecardResource,
team.NewTeamResource,
page.NewPageResource,
+ page_permissions.NewPagePermissionsResource,
}
}
From 393ce0f5f7289cadaedc60becd2eaa2e60d79abd Mon Sep 17 00:00:00 2001
From: OmriGez
Date: Thu, 29 Feb 2024 18:05:30 +0200
Subject: [PATCH 05/25] added support for array dataset
---
port/action/array.go | 19 +++-
port/action/model.go | 7 +-
port/action/refreshActionState.go | 9 +-
port/action/resource_test.go | 28 ++++++
port/action/schema.go | 160 ++++++++++++++++++++++++++++++
5 files changed, 216 insertions(+), 7 deletions(-)
diff --git a/port/action/array.go b/port/action/array.go
index 24dafd10..8d60ba8a 100644
--- a/port/action/array.go
+++ b/port/action/array.go
@@ -38,6 +38,10 @@ func handleArrayItemsToBody(ctx context.Context, property *cli.ActionProperty, p
items["enum"] = enumList
}
+ if !prop.StringItems.Dataset.IsNull() {
+ items["dataset"] = actionDataSetToPortBody(prop.StringItems.Dataset)
+ }
+
if !prop.StringItems.Format.IsNull() {
items["format"] = prop.StringItems.Format.ValueString()
}
@@ -76,6 +80,10 @@ func handleArrayItemsToBody(ctx context.Context, property *cli.ActionProperty, p
items["enum"] = enumList
}
+ if !prop.NumberItems.Dataset.IsNull() {
+ items["dataset"] = actionDataSetToPortBody(prop.NumberItems.Dataset)
+ }
+
if !prop.NumberItems.EnumJqQuery.IsNull() {
enumJqQueryMap := map[string]string{
"jqQuery": prop.NumberItems.EnumJqQuery.ValueString(),
@@ -98,6 +106,10 @@ func handleArrayItemsToBody(ctx context.Context, property *cli.ActionProperty, p
items["default"] = defaultList
}
+ if !prop.BooleanItems.Dataset.IsNull() {
+ items["dataset"] = actionDataSetToPortBody(prop.BooleanItems.Dataset)
+ }
+
property.Items = items
}
@@ -112,6 +124,10 @@ func handleArrayItemsToBody(ctx context.Context, property *cli.ActionProperty, p
items["default"] = defaultList
}
+ if !prop.ObjectItems.Dataset.IsNull() {
+ items["dataset"] = actionDataSetToPortBody(prop.ObjectItems.Dataset)
+ }
+
property.Items = items
}
return nil
@@ -165,9 +181,6 @@ func arrayPropResourceToBody(ctx context.Context, d *ActionModel, props map[stri
property.DependsOn = utils.InterfaceToStringArray(dependsOn)
}
- if prop.Dataset != nil {
- property.Dataset = actionDataSetToPortBody(prop.Dataset)
- }
err := handleArrayItemsToBody(ctx, &property, prop, required)
if err != nil {
diff --git a/port/action/model.go b/port/action/model.go
index ca017c73..31105f52 100644
--- a/port/action/model.go
+++ b/port/action/model.go
@@ -58,7 +58,6 @@ type StringPropModel struct {
DefaultJqQuery types.String `tfsdk:"default_jq_query"`
Visible types.Bool `tfsdk:"visible"`
VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
-
Default types.String `tfsdk:"default"`
Blueprint types.String `tfsdk:"blueprint"`
Format types.String `tfsdk:"format"`
@@ -108,11 +107,9 @@ type ArrayPropModel struct {
Description types.String `tfsdk:"description"`
Required types.Bool `tfsdk:"required"`
DependsOn types.List `tfsdk:"depends_on"`
- Dataset *DatasetModel `tfsdk:"dataset"`
DefaultJqQuery types.String `tfsdk:"default_jq_query"`
Visible types.Bool `tfsdk:"visible"`
VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
-
MaxItems types.Int64 `tfsdk:"max_items"`
MinItems types.Int64 `tfsdk:"min_items"`
StringItems *StringItems `tfsdk:"string_items"`
@@ -142,20 +139,24 @@ type StringItems struct {
Default types.List `tfsdk:"default"`
Enum types.List `tfsdk:"enum"`
EnumJqQuery types.String `tfsdk:"enum_jq_query"`
+ Dataset *DatasetModel `tfsdk:"dataset"`
}
type NumberItems struct {
Default types.List `tfsdk:"default"`
Enum types.List `tfsdk:"enum"`
EnumJqQuery types.String `tfsdk:"enum_jq_query"`
+ Dataset *DatasetModel `tfsdk:"dataset"`
}
type BooleanItems struct {
Default types.List `tfsdk:"default"`
+ Dataset *DatasetModel `tfsdk:"dataset"`
}
type ObjectItems struct {
Default types.List `tfsdk:"default"`
+ Dataset *DatasetModel `tfsdk:"dataset"`
}
type UserPropertiesModel struct {
diff --git a/port/action/refreshActionState.go b/port/action/refreshActionState.go
index 525b9734..141d1967 100644
--- a/port/action/refreshActionState.go
+++ b/port/action/refreshActionState.go
@@ -401,7 +401,14 @@ func setCommonProperties(ctx context.Context, v cli.ActionProperty, prop interfa
case *BooleanPropModel:
p.Dataset = dataset
case *ArrayPropModel:
- p.Dataset = dataset
+ if (!p.StringItems.IsNull())
+ p.StringItems.Dataset = dataset
+ if (!p.NumberItems.IsNull())
+ p.NumberItems.Dataset = dataset
+ if (!p.ObjectItems.IsNull())
+ p.ObjectItems.Dataset = dataset
+ if (!p.BooleanItems.IsNull())
+ p.BooleanItems.Dataset = dataset
case *ObjectPropModel:
p.Dataset = dataset
}
diff --git a/port/action/resource_test.go b/port/action/resource_test.go
index abda8d55..f3d4d50c 100644
--- a/port/action/resource_test.go
+++ b/port/action/resource_test.go
@@ -511,6 +511,30 @@ func TestAccPortActionAdvancedFormConfigurations(t *testing.T) {
]
}
}
+ array_props = {
+ myArrayPropIdentifier = {
+ title = "myArrayPropIdentifier"
+ required = true
+ format = "array"
+ blueprint = port_blueprint.microservice.id
+ string_items ={
+ blueprint = port_blueprint.microservice.id
+ format = "entity"
+ dataset = {
+ "combinator" : "and",
+ "rules" : [
+ {
+ "property" : "$identifier",
+ "operator" : "containsAny",
+ "value" : {
+ "jq_query" : "Test"
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
}
}
}`, actionIdentifier)
@@ -544,6 +568,10 @@ func TestAccPortActionAdvancedFormConfigurations(t *testing.T) {
resource.TestCheckResourceAttr("port_action.action1", "user_properties.string_props.myStringIdentifier3.dataset.rules.0.property", "$team"),
resource.TestCheckResourceAttr("port_action.action1", "user_properties.string_props.myStringIdentifier3.dataset.rules.0.operator", "containsAny"),
resource.TestCheckResourceAttr("port_action.action1", "user_properties.string_props.myStringIdentifier3.dataset.rules.0.value.jq_query", "Test"),
+ resource.TestCheckResourceAttr("port_action.action1", "user_properties.array_props.myArrayPropIdentifier.string_items.dataset.combinator", "and"),
+ resource.TestCheckResourceAttr("port_action.action1", "user_properties.array_props.myArrayPropIdentifier.string_items.dataset.rules.0.property", "$identifier"),
+ resource.TestCheckResourceAttr("port_action.action1", "user_properties.array_props.myArrayPropIdentifier.string_items.dataset.rules.0.operator", "containsAny"),
+ resource.TestCheckResourceAttr("port_action.action1", "user_properties.array_props.myArrayPropIdentifier.string_items.dataset.rules.0.value.jq_query", "Test"),
),
},
},
diff --git a/port/action/schema.go b/port/action/schema.go
index a9a6e29e..13aa1323 100644
--- a/port/action/schema.go
+++ b/port/action/schema.go
@@ -557,6 +557,46 @@ func ArrayPropertySchema() schema.Attribute {
listvalidator.SizeAtLeast(1),
},
},
+ "dataset": schema.SingleNestedAttribute{
+ MarkdownDescription: "The dataset of the property",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "combinator": schema.StringAttribute{
+ MarkdownDescription: "The combinator of the dataset",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("and", "or"),
+ },
+ },
+ "rules": schema.ListNestedAttribute{
+ MarkdownDescription: "The rules of the dataset",
+ Required: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "blueprint": schema.StringAttribute{
+ MarkdownDescription: "The blueprint identifier of the rule",
+ Optional: true,
+ },
+ "property": schema.StringAttribute{
+ MarkdownDescription: "The property identifier of the rule",
+ Optional: true,
+ },
+ "operator": schema.StringAttribute{
+ MarkdownDescription: "The operator of the rule",
+ Required: true,
+ },
+ "value": schema.ObjectAttribute{
+ MarkdownDescription: "The value of the rule",
+ Required: true,
+ AttributeTypes: map[string]attr.Type{
+ "jq_query": types.StringType,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
"enum_jq_query": schema.StringAttribute{
MarkdownDescription: "The enum jq query of the string items",
Optional: true,
@@ -584,6 +624,46 @@ func ArrayPropertySchema() schema.Attribute {
listvalidator.SizeAtLeast(1),
},
},
+ "dataset": schema.SingleNestedAttribute{
+ MarkdownDescription: "The dataset of the property",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "combinator": schema.StringAttribute{
+ MarkdownDescription: "The combinator of the dataset",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("and", "or"),
+ },
+ },
+ "rules": schema.ListNestedAttribute{
+ MarkdownDescription: "The rules of the dataset",
+ Required: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "blueprint": schema.StringAttribute{
+ MarkdownDescription: "The blueprint identifier of the rule",
+ Optional: true,
+ },
+ "property": schema.StringAttribute{
+ MarkdownDescription: "The property identifier of the rule",
+ Optional: true,
+ },
+ "operator": schema.StringAttribute{
+ MarkdownDescription: "The operator of the rule",
+ Required: true,
+ },
+ "value": schema.ObjectAttribute{
+ MarkdownDescription: "The value of the rule",
+ Required: true,
+ AttributeTypes: map[string]attr.Type{
+ "jq_query": types.StringType,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
"enum_jq_query": schema.StringAttribute{
MarkdownDescription: "The enum jq query of the number items",
Optional: true,
@@ -602,6 +682,46 @@ func ArrayPropertySchema() schema.Attribute {
Optional: true,
ElementType: types.BoolType,
},
+ "dataset": schema.SingleNestedAttribute{
+ MarkdownDescription: "The dataset of the property",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "combinator": schema.StringAttribute{
+ MarkdownDescription: "The combinator of the dataset",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("and", "or"),
+ },
+ },
+ "rules": schema.ListNestedAttribute{
+ MarkdownDescription: "The rules of the dataset",
+ Required: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "blueprint": schema.StringAttribute{
+ MarkdownDescription: "The blueprint identifier of the rule",
+ Optional: true,
+ },
+ "property": schema.StringAttribute{
+ MarkdownDescription: "The property identifier of the rule",
+ Optional: true,
+ },
+ "operator": schema.StringAttribute{
+ MarkdownDescription: "The operator of the rule",
+ Required: true,
+ },
+ "value": schema.ObjectAttribute{
+ MarkdownDescription: "The value of the rule",
+ Required: true,
+ AttributeTypes: map[string]attr.Type{
+ "jq_query": types.StringType,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
},
},
"object_items": schema.SingleNestedAttribute{
@@ -613,6 +733,46 @@ func ArrayPropertySchema() schema.Attribute {
Optional: true,
ElementType: types.MapType{ElemType: types.StringType},
},
+ "dataset": schema.SingleNestedAttribute{
+ MarkdownDescription: "The dataset of the property",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "combinator": schema.StringAttribute{
+ MarkdownDescription: "The combinator of the dataset",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("and", "or"),
+ },
+ },
+ "rules": schema.ListNestedAttribute{
+ MarkdownDescription: "The rules of the dataset",
+ Required: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "blueprint": schema.StringAttribute{
+ MarkdownDescription: "The blueprint identifier of the rule",
+ Optional: true,
+ },
+ "property": schema.StringAttribute{
+ MarkdownDescription: "The property identifier of the rule",
+ Optional: true,
+ },
+ "operator": schema.StringAttribute{
+ MarkdownDescription: "The operator of the rule",
+ Required: true,
+ },
+ "value": schema.ObjectAttribute{
+ MarkdownDescription: "The value of the rule",
+ Required: true,
+ AttributeTypes: map[string]attr.Type{
+ "jq_query": types.StringType,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
},
},
"visible": schema.BoolAttribute{
From f98593b884bec7ac4a14cd5b2ebb8d60ec59a510 Mon Sep 17 00:00:00 2001
From: Paz Hershberg
Date: Sun, 3 Mar 2024 13:28:59 +0200
Subject: [PATCH 06/25] syntax fix
---
port/action/refreshActionState.go | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/port/action/refreshActionState.go b/port/action/refreshActionState.go
index 141d1967..fc2a9256 100644
--- a/port/action/refreshActionState.go
+++ b/port/action/refreshActionState.go
@@ -3,9 +3,10 @@ package action
import (
"context"
"fmt"
- "github.com/samber/lo"
"reflect"
+ "github.com/samber/lo"
+
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/port-labs/terraform-provider-port-labs/internal/cli"
"github.com/port-labs/terraform-provider-port-labs/internal/consts"
@@ -401,14 +402,18 @@ func setCommonProperties(ctx context.Context, v cli.ActionProperty, prop interfa
case *BooleanPropModel:
p.Dataset = dataset
case *ArrayPropModel:
- if (!p.StringItems.IsNull())
+ if p.StringItems != nil {
p.StringItems.Dataset = dataset
- if (!p.NumberItems.IsNull())
+ }
+ if p.NumberItems != nil {
p.NumberItems.Dataset = dataset
- if (!p.ObjectItems.IsNull())
+ }
+ if p.ObjectItems != nil {
p.ObjectItems.Dataset = dataset
- if (!p.BooleanItems.IsNull())
+ }
+ if p.BooleanItems != nil {
p.BooleanItems.Dataset = dataset
+ }
case *ObjectPropModel:
p.Dataset = dataset
}
From 1a8ef996e5d055c3067b06f2442697df87be63f4 Mon Sep 17 00:00:00 2001
From: Paz Hershberg
Date: Sun, 3 Mar 2024 16:38:58 +0200
Subject: [PATCH 07/25] fixed some stuff
---
port/action/array.go | 14 +-
port/action/model.go | 31 +++--
port/action/refreshActionState.go | 9 --
port/action/schema.go | 217 +++++++-----------------------
4 files changed, 65 insertions(+), 206 deletions(-)
diff --git a/port/action/array.go b/port/action/array.go
index 8d60ba8a..704ddc14 100644
--- a/port/action/array.go
+++ b/port/action/array.go
@@ -38,7 +38,7 @@ func handleArrayItemsToBody(ctx context.Context, property *cli.ActionProperty, p
items["enum"] = enumList
}
- if !prop.StringItems.Dataset.IsNull() {
+ if prop.StringItems.Dataset != nil {
items["dataset"] = actionDataSetToPortBody(prop.StringItems.Dataset)
}
@@ -80,10 +80,6 @@ func handleArrayItemsToBody(ctx context.Context, property *cli.ActionProperty, p
items["enum"] = enumList
}
- if !prop.NumberItems.Dataset.IsNull() {
- items["dataset"] = actionDataSetToPortBody(prop.NumberItems.Dataset)
- }
-
if !prop.NumberItems.EnumJqQuery.IsNull() {
enumJqQueryMap := map[string]string{
"jqQuery": prop.NumberItems.EnumJqQuery.ValueString(),
@@ -106,10 +102,6 @@ func handleArrayItemsToBody(ctx context.Context, property *cli.ActionProperty, p
items["default"] = defaultList
}
- if !prop.BooleanItems.Dataset.IsNull() {
- items["dataset"] = actionDataSetToPortBody(prop.BooleanItems.Dataset)
- }
-
property.Items = items
}
@@ -124,10 +116,6 @@ func handleArrayItemsToBody(ctx context.Context, property *cli.ActionProperty, p
items["default"] = defaultList
}
- if !prop.ObjectItems.Dataset.IsNull() {
- items["dataset"] = actionDataSetToPortBody(prop.ObjectItems.Dataset)
- }
-
property.Items = items
}
return nil
diff --git a/port/action/model.go b/port/action/model.go
index 31105f52..db52da0e 100644
--- a/port/action/model.go
+++ b/port/action/model.go
@@ -58,6 +58,7 @@ type StringPropModel struct {
DefaultJqQuery types.String `tfsdk:"default_jq_query"`
Visible types.Bool `tfsdk:"visible"`
VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
+
Default types.String `tfsdk:"default"`
Blueprint types.String `tfsdk:"blueprint"`
Format types.String `tfsdk:"format"`
@@ -102,14 +103,15 @@ type BooleanPropModel struct {
}
type ArrayPropModel struct {
- Title types.String `tfsdk:"title"`
- Icon types.String `tfsdk:"icon"`
- Description types.String `tfsdk:"description"`
- Required types.Bool `tfsdk:"required"`
- DependsOn types.List `tfsdk:"depends_on"`
- DefaultJqQuery types.String `tfsdk:"default_jq_query"`
- Visible types.Bool `tfsdk:"visible"`
- VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
+ Title types.String `tfsdk:"title"`
+ Icon types.String `tfsdk:"icon"`
+ Description types.String `tfsdk:"description"`
+ Required types.Bool `tfsdk:"required"`
+ DependsOn types.List `tfsdk:"depends_on"`
+ DefaultJqQuery types.String `tfsdk:"default_jq_query"`
+ Visible types.Bool `tfsdk:"visible"`
+ VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
+
MaxItems types.Int64 `tfsdk:"max_items"`
MinItems types.Int64 `tfsdk:"min_items"`
StringItems *StringItems `tfsdk:"string_items"`
@@ -134,11 +136,11 @@ type ObjectPropModel struct {
}
type StringItems struct {
- Blueprint types.String `tfsdk:"blueprint"`
- Format types.String `tfsdk:"format"`
- Default types.List `tfsdk:"default"`
- Enum types.List `tfsdk:"enum"`
- EnumJqQuery types.String `tfsdk:"enum_jq_query"`
+ Blueprint types.String `tfsdk:"blueprint"`
+ Format types.String `tfsdk:"format"`
+ Default types.List `tfsdk:"default"`
+ Enum types.List `tfsdk:"enum"`
+ EnumJqQuery types.String `tfsdk:"enum_jq_query"`
Dataset *DatasetModel `tfsdk:"dataset"`
}
@@ -146,17 +148,14 @@ type NumberItems struct {
Default types.List `tfsdk:"default"`
Enum types.List `tfsdk:"enum"`
EnumJqQuery types.String `tfsdk:"enum_jq_query"`
- Dataset *DatasetModel `tfsdk:"dataset"`
}
type BooleanItems struct {
Default types.List `tfsdk:"default"`
- Dataset *DatasetModel `tfsdk:"dataset"`
}
type ObjectItems struct {
Default types.List `tfsdk:"default"`
- Dataset *DatasetModel `tfsdk:"dataset"`
}
type UserPropertiesModel struct {
diff --git a/port/action/refreshActionState.go b/port/action/refreshActionState.go
index fc2a9256..417f7a20 100644
--- a/port/action/refreshActionState.go
+++ b/port/action/refreshActionState.go
@@ -405,15 +405,6 @@ func setCommonProperties(ctx context.Context, v cli.ActionProperty, prop interfa
if p.StringItems != nil {
p.StringItems.Dataset = dataset
}
- if p.NumberItems != nil {
- p.NumberItems.Dataset = dataset
- }
- if p.ObjectItems != nil {
- p.ObjectItems.Dataset = dataset
- }
- if p.BooleanItems != nil {
- p.BooleanItems.Dataset = dataset
- }
case *ObjectPropModel:
p.Dataset = dataset
}
diff --git a/port/action/schema.go b/port/action/schema.go
index 13aa1323..193b13cd 100644
--- a/port/action/schema.go
+++ b/port/action/schema.go
@@ -3,6 +3,7 @@ package action
import (
"context"
"fmt"
+
"github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
@@ -43,46 +44,6 @@ func MetadataProperties() map[string]schema.Attribute {
Optional: true,
ElementType: types.StringType,
},
- "dataset": schema.SingleNestedAttribute{
- MarkdownDescription: "The dataset of the property",
- Optional: true,
- Attributes: map[string]schema.Attribute{
- "combinator": schema.StringAttribute{
- MarkdownDescription: "The combinator of the dataset",
- Required: true,
- Validators: []validator.String{
- stringvalidator.OneOf("and", "or"),
- },
- },
- "rules": schema.ListNestedAttribute{
- MarkdownDescription: "The rules of the dataset",
- Required: true,
- NestedObject: schema.NestedAttributeObject{
- Attributes: map[string]schema.Attribute{
- "blueprint": schema.StringAttribute{
- MarkdownDescription: "The blueprint identifier of the rule",
- Optional: true,
- },
- "property": schema.StringAttribute{
- MarkdownDescription: "The property identifier of the rule",
- Optional: true,
- },
- "operator": schema.StringAttribute{
- MarkdownDescription: "The operator of the rule",
- Required: true,
- },
- "value": schema.ObjectAttribute{
- MarkdownDescription: "The value of the rule",
- Required: true,
- AttributeTypes: map[string]attr.Type{
- "jq_query": types.StringType,
- },
- },
- },
- },
- },
- },
- },
}
}
@@ -355,6 +316,46 @@ func StringPropertySchema() schema.Attribute {
stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("visible")),
},
},
+ "dataset": schema.SingleNestedAttribute{
+ MarkdownDescription: "The dataset of an the entity-format property",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "combinator": schema.StringAttribute{
+ MarkdownDescription: "The combinator of the dataset",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("and", "or"),
+ },
+ },
+ "rules": schema.ListNestedAttribute{
+ MarkdownDescription: "The rules of the dataset",
+ Required: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "blueprint": schema.StringAttribute{
+ MarkdownDescription: "The blueprint identifier of the rule",
+ Optional: true,
+ },
+ "property": schema.StringAttribute{
+ MarkdownDescription: "The property identifier of the rule",
+ Optional: true,
+ },
+ "operator": schema.StringAttribute{
+ MarkdownDescription: "The operator of the rule",
+ Required: true,
+ },
+ "value": schema.ObjectAttribute{
+ MarkdownDescription: "The value of the rule",
+ Required: true,
+ AttributeTypes: map[string]attr.Type{
+ "jq_query": types.StringType,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
}
utils.CopyMaps(stringPropertySchema, MetadataProperties())
@@ -557,8 +558,15 @@ func ArrayPropertySchema() schema.Attribute {
listvalidator.SizeAtLeast(1),
},
},
+ "enum_jq_query": schema.StringAttribute{
+ MarkdownDescription: "The enum jq query of the string items",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("enum")),
+ },
+ },
"dataset": schema.SingleNestedAttribute{
- MarkdownDescription: "The dataset of the property",
+ MarkdownDescription: "The dataset of an the entity-format property",
Optional: true,
Attributes: map[string]schema.Attribute{
"combinator": schema.StringAttribute{
@@ -597,13 +605,6 @@ func ArrayPropertySchema() schema.Attribute {
},
},
},
- "enum_jq_query": schema.StringAttribute{
- MarkdownDescription: "The enum jq query of the string items",
- Optional: true,
- Validators: []validator.String{
- stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("enum")),
- },
- },
},
},
"number_items": schema.SingleNestedAttribute{
@@ -624,46 +625,6 @@ func ArrayPropertySchema() schema.Attribute {
listvalidator.SizeAtLeast(1),
},
},
- "dataset": schema.SingleNestedAttribute{
- MarkdownDescription: "The dataset of the property",
- Optional: true,
- Attributes: map[string]schema.Attribute{
- "combinator": schema.StringAttribute{
- MarkdownDescription: "The combinator of the dataset",
- Required: true,
- Validators: []validator.String{
- stringvalidator.OneOf("and", "or"),
- },
- },
- "rules": schema.ListNestedAttribute{
- MarkdownDescription: "The rules of the dataset",
- Required: true,
- NestedObject: schema.NestedAttributeObject{
- Attributes: map[string]schema.Attribute{
- "blueprint": schema.StringAttribute{
- MarkdownDescription: "The blueprint identifier of the rule",
- Optional: true,
- },
- "property": schema.StringAttribute{
- MarkdownDescription: "The property identifier of the rule",
- Optional: true,
- },
- "operator": schema.StringAttribute{
- MarkdownDescription: "The operator of the rule",
- Required: true,
- },
- "value": schema.ObjectAttribute{
- MarkdownDescription: "The value of the rule",
- Required: true,
- AttributeTypes: map[string]attr.Type{
- "jq_query": types.StringType,
- },
- },
- },
- },
- },
- },
- },
"enum_jq_query": schema.StringAttribute{
MarkdownDescription: "The enum jq query of the number items",
Optional: true,
@@ -682,46 +643,6 @@ func ArrayPropertySchema() schema.Attribute {
Optional: true,
ElementType: types.BoolType,
},
- "dataset": schema.SingleNestedAttribute{
- MarkdownDescription: "The dataset of the property",
- Optional: true,
- Attributes: map[string]schema.Attribute{
- "combinator": schema.StringAttribute{
- MarkdownDescription: "The combinator of the dataset",
- Required: true,
- Validators: []validator.String{
- stringvalidator.OneOf("and", "or"),
- },
- },
- "rules": schema.ListNestedAttribute{
- MarkdownDescription: "The rules of the dataset",
- Required: true,
- NestedObject: schema.NestedAttributeObject{
- Attributes: map[string]schema.Attribute{
- "blueprint": schema.StringAttribute{
- MarkdownDescription: "The blueprint identifier of the rule",
- Optional: true,
- },
- "property": schema.StringAttribute{
- MarkdownDescription: "The property identifier of the rule",
- Optional: true,
- },
- "operator": schema.StringAttribute{
- MarkdownDescription: "The operator of the rule",
- Required: true,
- },
- "value": schema.ObjectAttribute{
- MarkdownDescription: "The value of the rule",
- Required: true,
- AttributeTypes: map[string]attr.Type{
- "jq_query": types.StringType,
- },
- },
- },
- },
- },
- },
- },
},
},
"object_items": schema.SingleNestedAttribute{
@@ -733,46 +654,6 @@ func ArrayPropertySchema() schema.Attribute {
Optional: true,
ElementType: types.MapType{ElemType: types.StringType},
},
- "dataset": schema.SingleNestedAttribute{
- MarkdownDescription: "The dataset of the property",
- Optional: true,
- Attributes: map[string]schema.Attribute{
- "combinator": schema.StringAttribute{
- MarkdownDescription: "The combinator of the dataset",
- Required: true,
- Validators: []validator.String{
- stringvalidator.OneOf("and", "or"),
- },
- },
- "rules": schema.ListNestedAttribute{
- MarkdownDescription: "The rules of the dataset",
- Required: true,
- NestedObject: schema.NestedAttributeObject{
- Attributes: map[string]schema.Attribute{
- "blueprint": schema.StringAttribute{
- MarkdownDescription: "The blueprint identifier of the rule",
- Optional: true,
- },
- "property": schema.StringAttribute{
- MarkdownDescription: "The property identifier of the rule",
- Optional: true,
- },
- "operator": schema.StringAttribute{
- MarkdownDescription: "The operator of the rule",
- Required: true,
- },
- "value": schema.ObjectAttribute{
- MarkdownDescription: "The value of the rule",
- Required: true,
- AttributeTypes: map[string]attr.Type{
- "jq_query": types.StringType,
- },
- },
- },
- },
- },
- },
- },
},
},
"visible": schema.BoolAttribute{
From 87058d3c94e34e2d6373c0f15a22ad8489977725 Mon Sep 17 00:00:00 2001
From: Paz Hershberg
Date: Mon, 4 Mar 2024 11:34:20 +0200
Subject: [PATCH 08/25] some changes
---
port/action/array.go | 3 +++
port/action/boolean.go | 3 ---
port/action/number.go | 4 ----
port/action/object.go | 4 ----
port/action/refreshActionState.go | 29 ++++-------------------------
port/action/resource_test.go | 3 +--
port/action/schema.go | 2 +-
port/action/string.go | 1 +
8 files changed, 10 insertions(+), 39 deletions(-)
diff --git a/port/action/array.go b/port/action/array.go
index 704ddc14..a4c857c8 100644
--- a/port/action/array.go
+++ b/port/action/array.go
@@ -234,6 +234,9 @@ func addArrayPropertiesToResource(v *cli.ActionProperty) (*ArrayPropModel, error
if value, ok := v.Items["blueprint"]; ok && value != nil {
arrayProp.StringItems.Blueprint = types.StringValue(v.Items["blueprint"].(string))
}
+ if value, ok := v.Items["dataset"]; ok && value != nil {
+ arrayProp.StringItems.Dataset = writeDatasetToResource(v.Items["dataset"].(*cli.Dataset))
+ }
if value, ok := v.Items["enum"]; ok && value != nil {
v := reflect.ValueOf(value)
diff --git a/port/action/boolean.go b/port/action/boolean.go
index 1b5b7263..51a95457 100644
--- a/port/action/boolean.go
+++ b/port/action/boolean.go
@@ -49,9 +49,6 @@ func booleanPropResourceToBody(ctx context.Context, d *ActionModel, props map[st
property.DependsOn = utils.InterfaceToStringArray(dependsOn)
}
- if prop.Dataset != nil {
- property.Dataset = actionDataSetToPortBody(prop.Dataset)
- }
if !prop.Visible.IsNull() {
property.Visible = prop.Visible.ValueBoolPointer()
diff --git a/port/action/number.go b/port/action/number.go
index a5dfa73a..4bd30cfe 100644
--- a/port/action/number.go
+++ b/port/action/number.go
@@ -81,10 +81,6 @@ func numberPropResourceToBody(ctx context.Context, state *ActionModel, props map
}
- if prop.Dataset != nil {
- property.Dataset = actionDataSetToPortBody(prop.Dataset)
- }
-
if !prop.Visible.IsNull() {
property.Visible = prop.Visible.ValueBoolPointer()
}
diff --git a/port/action/object.go b/port/action/object.go
index 49ac66a1..2c2f2d6d 100644
--- a/port/action/object.go
+++ b/port/action/object.go
@@ -63,10 +63,6 @@ func objectPropResourceToBody(ctx context.Context, d *ActionModel, props map[str
property.Encryption = &encryption
}
- if prop.Dataset != nil {
- property.Dataset = actionDataSetToPortBody(prop.Dataset)
- }
-
if !prop.Visible.IsNull() {
property.Visible = prop.Visible.ValueBoolPointer()
}
diff --git a/port/action/refreshActionState.go b/port/action/refreshActionState.go
index 417f7a20..1eb37d17 100644
--- a/port/action/refreshActionState.go
+++ b/port/action/refreshActionState.go
@@ -58,18 +58,16 @@ func writeInvocationMethodToResource(a *cli.Action, state *ActionModel) {
}
}
-func writeDatasetToResource(v cli.ActionProperty) *DatasetModel {
- if v.Dataset == nil {
+func writeDatasetToResource(ds *cli.Dataset) *DatasetModel {
+ if ds == nil {
return nil
}
- dataset := v.Dataset
-
datasetModel := &DatasetModel{
- Combinator: types.StringValue(dataset.Combinator),
+ Combinator: types.StringValue(ds.Combinator),
}
- for _, v := range dataset.Rules {
+ for _, v := range ds.Rules {
rule := &Rule{
Blueprint: flex.GoStringToFramework(v.Blueprint),
Property: flex.GoStringToFramework(v.Property),
@@ -391,25 +389,6 @@ func setCommonProperties(ctx context.Context, v cli.ActionProperty, prop interfa
p.DependsOn = flex.GoArrayStringToTerraformList(ctx, v.DependsOn)
}
- case "Dataset":
- dataset := writeDatasetToResource(v)
- if dataset != nil {
- switch p := prop.(type) {
- case *StringPropModel:
- p.Dataset = dataset
- case *NumberPropModel:
- p.Dataset = dataset
- case *BooleanPropModel:
- p.Dataset = dataset
- case *ArrayPropModel:
- if p.StringItems != nil {
- p.StringItems.Dataset = dataset
- }
- case *ObjectPropModel:
- p.Dataset = dataset
- }
- }
-
case "Visible":
visible, visibleJq := writeVisibleToResource(v)
if !visible.IsNull() {
diff --git a/port/action/resource_test.go b/port/action/resource_test.go
index f3d4d50c..ae6437bf 100644
--- a/port/action/resource_test.go
+++ b/port/action/resource_test.go
@@ -515,9 +515,8 @@ func TestAccPortActionAdvancedFormConfigurations(t *testing.T) {
myArrayPropIdentifier = {
title = "myArrayPropIdentifier"
required = true
- format = "array"
blueprint = port_blueprint.microservice.id
- string_items ={
+ string_items = {
blueprint = port_blueprint.microservice.id
format = "entity"
dataset = {
diff --git a/port/action/schema.go b/port/action/schema.go
index 193b13cd..9cccaeaf 100644
--- a/port/action/schema.go
+++ b/port/action/schema.go
@@ -566,7 +566,7 @@ func ArrayPropertySchema() schema.Attribute {
},
},
"dataset": schema.SingleNestedAttribute{
- MarkdownDescription: "The dataset of an the entity-format property",
+ MarkdownDescription: "The dataset of an the entity-format items",
Optional: true,
Attributes: map[string]schema.Attribute{
"combinator": schema.StringAttribute{
diff --git a/port/action/string.go b/port/action/string.go
index 8e967e13..dd7442d1 100644
--- a/port/action/string.go
+++ b/port/action/string.go
@@ -131,6 +131,7 @@ func addStringPropertiesToResource(ctx context.Context, v *cli.ActionProperty) *
Format: flex.GoStringToFramework(v.Format),
Blueprint: flex.GoStringToFramework(v.Blueprint),
Encryption: flex.GoStringToFramework(v.Encryption),
+ Dataset: writeDatasetToResource(v.Dataset),
}
if v.Enum != nil {
From c78ea0f83cf195e869cd34b75551ec55fc8cb93d Mon Sep 17 00:00:00 2001
From: Paz Hershberg
Date: Mon, 4 Mar 2024 11:44:04 +0200
Subject: [PATCH 09/25] fixed models
---
port/action/model.go | 51 +++++++++++++++++++++-----------------------
1 file changed, 24 insertions(+), 27 deletions(-)
diff --git a/port/action/model.go b/port/action/model.go
index db52da0e..5e76bbcc 100644
--- a/port/action/model.go
+++ b/port/action/model.go
@@ -71,15 +71,14 @@ type StringPropModel struct {
}
type NumberPropModel struct {
- Title types.String `tfsdk:"title"`
- Icon types.String `tfsdk:"icon"`
- Description types.String `tfsdk:"description"`
- Required types.Bool `tfsdk:"required"`
- DependsOn types.List `tfsdk:"depends_on"`
- Dataset *DatasetModel `tfsdk:"dataset"`
- DefaultJqQuery types.String `tfsdk:"default_jq_query"`
- Visible types.Bool `tfsdk:"visible"`
- VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
+ Title types.String `tfsdk:"title"`
+ Icon types.String `tfsdk:"icon"`
+ Description types.String `tfsdk:"description"`
+ Required types.Bool `tfsdk:"required"`
+ DependsOn types.List `tfsdk:"depends_on"`
+ DefaultJqQuery types.String `tfsdk:"default_jq_query"`
+ Visible types.Bool `tfsdk:"visible"`
+ VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
Default types.Float64 `tfsdk:"default"`
Maximum types.Float64 `tfsdk:"maximum"`
@@ -89,15 +88,14 @@ type NumberPropModel struct {
}
type BooleanPropModel struct {
- Title types.String `tfsdk:"title"`
- Icon types.String `tfsdk:"icon"`
- Description types.String `tfsdk:"description"`
- Required types.Bool `tfsdk:"required"`
- DependsOn types.List `tfsdk:"depends_on"`
- Dataset *DatasetModel `tfsdk:"dataset"`
- DefaultJqQuery types.String `tfsdk:"default_jq_query"`
- Visible types.Bool `tfsdk:"visible"`
- VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
+ Title types.String `tfsdk:"title"`
+ Icon types.String `tfsdk:"icon"`
+ Description types.String `tfsdk:"description"`
+ Required types.Bool `tfsdk:"required"`
+ DependsOn types.List `tfsdk:"depends_on"`
+ DefaultJqQuery types.String `tfsdk:"default_jq_query"`
+ Visible types.Bool `tfsdk:"visible"`
+ VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
Default types.Bool `tfsdk:"default"`
}
@@ -121,15 +119,14 @@ type ArrayPropModel struct {
}
type ObjectPropModel struct {
- Title types.String `tfsdk:"title"`
- Icon types.String `tfsdk:"icon"`
- Description types.String `tfsdk:"description"`
- Required types.Bool `tfsdk:"required"`
- DependsOn types.List `tfsdk:"depends_on"`
- Dataset *DatasetModel `tfsdk:"dataset"`
- DefaultJqQuery types.String `tfsdk:"default_jq_query"`
- Visible types.Bool `tfsdk:"visible"`
- VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
+ Title types.String `tfsdk:"title"`
+ Icon types.String `tfsdk:"icon"`
+ Description types.String `tfsdk:"description"`
+ Required types.Bool `tfsdk:"required"`
+ DependsOn types.List `tfsdk:"depends_on"`
+ DefaultJqQuery types.String `tfsdk:"default_jq_query"`
+ Visible types.Bool `tfsdk:"visible"`
+ VisibleJqQuery types.String `tfsdk:"visible_jq_query"`
Default types.String `tfsdk:"default"`
Encryption types.String `tfsdk:"encryption"`
From 671712da283397446347a127ea0cbe69a53fe83a Mon Sep 17 00:00:00 2001
From: Paz Hershberg
Date: Tue, 5 Mar 2024 17:28:24 +0200
Subject: [PATCH 10/25] implemented the new string-dataset for array-string
properties
---
internal/utils/utils.go | 13 ++
.../actionPermissionToPortBody.go | 22 +---
port/action/array.go | 15 ++-
port/action/model.go | 12 +-
port/action/resource_test.go | 123 +++++++++---------
port/action/schema.go | 38 +-----
.../readStateToPortBody.go | 17 +--
port/blueprint/schema.go | 1 +
port/page/pageToPortBody.go | 10 +-
9 files changed, 106 insertions(+), 145 deletions(-)
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
index f012d021..f1407731 100644
--- a/internal/utils/utils.go
+++ b/internal/utils/utils.go
@@ -85,6 +85,19 @@ func GoObjectToTerraformString(v interface{}) (types.String, error) {
return types.StringValue(value), nil
}
+func TerraformJsonStringToGoObject(v *string) (*map[string]any, error) {
+ if v == nil || *v == "" {
+ return nil, nil
+ }
+
+ vMap := make(map[string]any)
+ if err := json.Unmarshal([]byte(*v), &vMap); err != nil {
+ return nil, err
+ }
+
+ return &vMap, nil
+}
+
func InterfaceToStringArray(o interface{}) []string {
items := o.([]interface{})
res := make([]string, len(items))
diff --git a/port/action-permissions/actionPermissionToPortBody.go b/port/action-permissions/actionPermissionToPortBody.go
index d5d996a9..00d93904 100644
--- a/port/action-permissions/actionPermissionToPortBody.go
+++ b/port/action-permissions/actionPermissionToPortBody.go
@@ -1,27 +1,11 @@
package action_permissions
import (
- "encoding/json"
"github.com/port-labs/terraform-provider-port-labs/internal/cli"
"github.com/port-labs/terraform-provider-port-labs/internal/flex"
+ "github.com/port-labs/terraform-provider-port-labs/internal/utils"
)
-func policyToPortBody(policy *string) (*map[string]any, error) {
- // if policy is empty, set it to nil, so it will override the existing policy on server,
- // as opposed to merging it, due to only having a PATCH endpoint
-
- if policy == nil || *policy == "" {
- return nil, nil
- }
-
- policyMap := make(map[string]any)
- if err := json.Unmarshal([]byte(*policy), &policyMap); err != nil {
- return nil, err
- }
-
- return &policyMap, nil
-}
-
func actionPermissionsToPortBody(state *PermissionsModel) (*cli.ActionPermissions, error) {
if state == nil {
return nil, nil
@@ -41,12 +25,12 @@ func actionPermissionsToPortBody(state *PermissionsModel) (*cli.ActionPermission
},
}
- approvePolicyMap, err := policyToPortBody(state.Approve.Policy.ValueStringPointer())
+ approvePolicyMap, err := utils.TerraformJsonStringToGoObject(state.Approve.Policy.ValueStringPointer())
if err != nil {
return nil, err
}
- executePolicyMap, err := policyToPortBody(state.Execute.Policy.ValueStringPointer())
+ executePolicyMap, err := utils.TerraformJsonStringToGoObject(state.Execute.Policy.ValueStringPointer())
if err != nil {
return nil, err
}
diff --git a/port/action/array.go b/port/action/array.go
index a4c857c8..c4149107 100644
--- a/port/action/array.go
+++ b/port/action/array.go
@@ -38,8 +38,13 @@ func handleArrayItemsToBody(ctx context.Context, property *cli.ActionProperty, p
items["enum"] = enumList
}
- if prop.StringItems.Dataset != nil {
- items["dataset"] = actionDataSetToPortBody(prop.StringItems.Dataset)
+ if !prop.StringItems.Dataset.IsNull() {
+ v, err := utils.TerraformJsonStringToGoObject(prop.StringItems.Dataset.ValueStringPointer())
+ if err != nil {
+ return err
+ }
+
+ items["dataset"] = v
}
if !prop.StringItems.Format.IsNull() {
@@ -235,7 +240,11 @@ func addArrayPropertiesToResource(v *cli.ActionProperty) (*ArrayPropModel, error
arrayProp.StringItems.Blueprint = types.StringValue(v.Items["blueprint"].(string))
}
if value, ok := v.Items["dataset"]; ok && value != nil {
- arrayProp.StringItems.Dataset = writeDatasetToResource(v.Items["dataset"].(*cli.Dataset))
+ ds, err := utils.GoObjectToTerraformString(v.Items["dataset"])
+ if err != nil {
+ return nil, err
+ }
+ arrayProp.StringItems.Dataset = ds
}
if value, ok := v.Items["enum"]; ok && value != nil {
diff --git a/port/action/model.go b/port/action/model.go
index 5e76bbcc..5123375b 100644
--- a/port/action/model.go
+++ b/port/action/model.go
@@ -133,12 +133,12 @@ type ObjectPropModel struct {
}
type StringItems struct {
- Blueprint types.String `tfsdk:"blueprint"`
- Format types.String `tfsdk:"format"`
- Default types.List `tfsdk:"default"`
- Enum types.List `tfsdk:"enum"`
- EnumJqQuery types.String `tfsdk:"enum_jq_query"`
- Dataset *DatasetModel `tfsdk:"dataset"`
+ Blueprint types.String `tfsdk:"blueprint"`
+ Format types.String `tfsdk:"format"`
+ Default types.List `tfsdk:"default"`
+ Enum types.List `tfsdk:"enum"`
+ EnumJqQuery types.String `tfsdk:"enum_jq_query"`
+ Dataset types.String `tfsdk:"dataset"`
}
type NumberItems struct {
diff --git a/port/action/resource_test.go b/port/action/resource_test.go
index ae6437bf..7332137e 100644
--- a/port/action/resource_test.go
+++ b/port/action/resource_test.go
@@ -470,73 +470,74 @@ func TestAccPortActionAdvancedFormConfigurations(t *testing.T) {
identifier := utils.GenID()
actionIdentifier := utils.GenID()
var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
- resource "port_action" "action1" {
- title = "Action 1"
- blueprint = port_blueprint.microservice.id
- identifier = "%s"
- trigger = "DAY-2"
- description = "This is a test action"
- required_approval = true
- github_method = {
- org = "port-labs"
- repo = "Port"
- workflow = "lint"
+
+resource "port_action" "action1" {
+ title = "Action 1"
+ blueprint = port_blueprint.microservice.id
+ identifier = "%s"
+ trigger = "DAY-2"
+ description = "This is a test action"
+ required_approval = true
+ github_method = {
+ org = "port-labs"
+ repo = "Port"
+ workflow = "lint"
+ }
+ user_properties = {
+ string_props = {
+ myStringIdentifier = {
+ title = "myStringIdentifier"
+ default = "default"
}
- user_properties = {
- string_props = {
- myStringIdentifier = {
- title = "myStringIdentifier"
- default = "default"
- }
- myStringIdentifier2 = {
- title = "myStringIdentifier2"
- default = "default"
- depends_on = ["myStringIdentifier"]
- }
- myStringIdentifier3 = {
- title = "myStringIdentifier3"
- required = true
- format = "entity"
- blueprint = port_blueprint.microservice.id
- dataset = {
- "combinator" : "and",
- "rules" : [
- {
- "property" : "$team",
- "operator" : "containsAny",
- "value" : {
- "jq_query" : "Test"
- }
- }
- ]
+ myStringIdentifier2 = {
+ title = "myStringIdentifier2"
+ default = "default"
+ depends_on = ["myStringIdentifier"]
+ }
+ myStringIdentifier3 = {
+ title = "myStringIdentifier3"
+ required = true
+ format = "entity"
+ blueprint = port_blueprint.microservice.id
+ dataset = {
+ "combinator" : "and",
+ "rules" : [
+ {
+ "property" : "$team",
+ "operator" : "containsAny",
+ "value" : {
+ "jq_query" : "Test"
+ }
}
- }
- array_props = {
- myArrayPropIdentifier = {
- title = "myArrayPropIdentifier"
- required = true
- blueprint = port_blueprint.microservice.id
- string_items = {
- blueprint = port_blueprint.microservice.id
- format = "entity"
- dataset = {
- "combinator" : "and",
- "rules" : [
- {
- "property" : "$identifier",
- "operator" : "containsAny",
- "value" : {
- "jq_query" : "Test"
- }
- }
- ]
- }
- }
+ ]
+ }
+ }
+ }
+ array_props = {
+ myArrayPropIdentifier = {
+ title = "myArrayPropIdentifier"
+ required = true
+ blueprint = port_blueprint.microservice.id
+ string_items = {
+ blueprint = port_blueprint.microservice.id
+ format = "entity"
+ dataset = {
+ "combinator" : "and",
+ "rules" : [
+ {
+ "property" : "$identifier",
+ "operator" : "containsAny",
+ "value" : {
+ "jq_query" : "Test"
+ }
}
+ ]
}
}
}
- }`, actionIdentifier)
+ }
+ }
+ }`, actionIdentifier)
resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.TestAccPreCheck(t) },
diff --git a/port/action/schema.go b/port/action/schema.go
index 9cccaeaf..a97b2bef 100644
--- a/port/action/schema.go
+++ b/port/action/schema.go
@@ -565,45 +565,9 @@ func ArrayPropertySchema() schema.Attribute {
stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("enum")),
},
},
- "dataset": schema.SingleNestedAttribute{
+ "dataset": schema.StringAttribute{
MarkdownDescription: "The dataset of an the entity-format items",
Optional: true,
- Attributes: map[string]schema.Attribute{
- "combinator": schema.StringAttribute{
- MarkdownDescription: "The combinator of the dataset",
- Required: true,
- Validators: []validator.String{
- stringvalidator.OneOf("and", "or"),
- },
- },
- "rules": schema.ListNestedAttribute{
- MarkdownDescription: "The rules of the dataset",
- Required: true,
- NestedObject: schema.NestedAttributeObject{
- Attributes: map[string]schema.Attribute{
- "blueprint": schema.StringAttribute{
- MarkdownDescription: "The blueprint identifier of the rule",
- Optional: true,
- },
- "property": schema.StringAttribute{
- MarkdownDescription: "The property identifier of the rule",
- Optional: true,
- },
- "operator": schema.StringAttribute{
- MarkdownDescription: "The operator of the rule",
- Required: true,
- },
- "value": schema.ObjectAttribute{
- MarkdownDescription: "The value of the rule",
- Required: true,
- AttributeTypes: map[string]attr.Type{
- "jq_query": types.StringType,
- },
- },
- },
- },
- },
- },
},
},
},
diff --git a/port/aggregation-properties/readStateToPortBody.go b/port/aggregation-properties/readStateToPortBody.go
index b6445291..809836c9 100644
--- a/port/aggregation-properties/readStateToPortBody.go
+++ b/port/aggregation-properties/readStateToPortBody.go
@@ -1,8 +1,8 @@
package aggregation_properties
import (
- "encoding/json"
"github.com/port-labs/terraform-provider-port-labs/internal/cli"
+ "github.com/port-labs/terraform-provider-port-labs/internal/utils"
)
func aggregationPropertiesToBody(state *AggregationPropertiesModel) (*map[string]cli.BlueprintAggregationProperty, error) {
@@ -49,7 +49,7 @@ func aggregationPropertiesToBody(state *AggregationPropertiesModel) (*map[string
}
}
- query, err := queryToPortBody(aggregationProperty.Query.ValueStringPointer())
+ query, err := utils.TerraformJsonStringToGoObject(aggregationProperty.Query.ValueStringPointer())
if err != nil {
return nil, err
@@ -66,16 +66,3 @@ func aggregationPropertiesToBody(state *AggregationPropertiesModel) (*map[string
return &aggregationProperties, nil
}
-
-func queryToPortBody(query *string) (*map[string]any, error) {
- if query == nil || *query == "" {
- return nil, nil
- }
-
- queryMap := make(map[string]any)
- if err := json.Unmarshal([]byte(*query), &queryMap); err != nil {
- return nil, err
- }
-
- return &queryMap, nil
-}
diff --git a/port/blueprint/schema.go b/port/blueprint/schema.go
index f789e9b7..c9008ea8 100644
--- a/port/blueprint/schema.go
+++ b/port/blueprint/schema.go
@@ -2,6 +2,7 @@ package blueprint
import (
"context"
+
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
diff --git a/port/page/pageToPortBody.go b/port/page/pageToPortBody.go
index 5c909792..d3c4e506 100644
--- a/port/page/pageToPortBody.go
+++ b/port/page/pageToPortBody.go
@@ -1,9 +1,9 @@
package page
import (
- "encoding/json"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/port-labs/terraform-provider-port-labs/internal/cli"
+ "github.com/port-labs/terraform-provider-port-labs/internal/utils"
)
func PageToPortBody(pm *PageModel) (*cli.Page, error) {
@@ -33,11 +33,13 @@ func widgetsToPortBody(widgets []types.String) (*[]map[string]any, error) {
}
widgetsBody := make([]map[string]any, len(widgets))
for i, w := range widgets {
- var widgetObject map[string]any
- if err := json.Unmarshal([]byte(w.ValueString()), &widgetObject); err != nil {
+ v, err := utils.TerraformJsonStringToGoObject(w.ValueStringPointer())
+
+ if err != nil {
return nil, err
}
- widgetsBody[i] = widgetObject
+
+ widgetsBody[i] = *v
}
return &widgetsBody, nil
From e78688db9409cddc161c4a0329e7a76b2bffc0a0 Mon Sep 17 00:00:00 2001
From: Paz Hershberg
Date: Tue, 5 Mar 2024 17:54:08 +0200
Subject: [PATCH 11/25] fixed array stringprop dataset structure in the tests
---
port/action/resource_test.go | 14 ++++----------
1 file changed, 4 insertions(+), 10 deletions(-)
diff --git a/port/action/resource_test.go b/port/action/resource_test.go
index 7332137e..8ff6a39c 100644
--- a/port/action/resource_test.go
+++ b/port/action/resource_test.go
@@ -521,18 +521,16 @@ resource "port_action" "action1" {
string_items = {
blueprint = port_blueprint.microservice.id
format = "entity"
- dataset = {
+ dataset = jsonencode({
"combinator" : "and",
"rules" : [
{
"property" : "$identifier",
"operator" : "containsAny",
- "value" : {
- "jq_query" : "Test"
- }
+ "value" : "Test"
}
]
- }
+ })
}
}
}
@@ -568,11 +566,7 @@ resource "port_action" "action1" {
resource.TestCheckResourceAttr("port_action.action1", "user_properties.string_props.myStringIdentifier3.dataset.rules.0.property", "$team"),
resource.TestCheckResourceAttr("port_action.action1", "user_properties.string_props.myStringIdentifier3.dataset.rules.0.operator", "containsAny"),
resource.TestCheckResourceAttr("port_action.action1", "user_properties.string_props.myStringIdentifier3.dataset.rules.0.value.jq_query", "Test"),
- resource.TestCheckResourceAttr("port_action.action1", "user_properties.array_props.myArrayPropIdentifier.string_items.dataset.combinator", "and"),
- resource.TestCheckResourceAttr("port_action.action1", "user_properties.array_props.myArrayPropIdentifier.string_items.dataset.rules.0.property", "$identifier"),
- resource.TestCheckResourceAttr("port_action.action1", "user_properties.array_props.myArrayPropIdentifier.string_items.dataset.rules.0.operator", "containsAny"),
- resource.TestCheckResourceAttr("port_action.action1", "user_properties.array_props.myArrayPropIdentifier.string_items.dataset.rules.0.value.jq_query", "Test"),
- ),
+ resource.TestCheckResourceAttr("port_action.action1", "user_properties.array_props.myArrayPropIdentifier.string_items.dataset", "{\"combinator\":\"and\",\"rules\":[{\"operator\":\"containsAny\",\"property\":\"$identifier\",\"value\":\"Test\"}]}")),
},
},
})
From 86ae2c73cf7dbe1a458818cf5d56ce3fc97ec6f7 Mon Sep 17 00:00:00 2001
From: Paz Hershberg
Date: Wed, 6 Mar 2024 11:57:10 +0200
Subject: [PATCH 12/25] started md docs to action schema
---
port/action/schema.go | 118 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 118 insertions(+)
diff --git a/port/action/schema.go b/port/action/schema.go
index a97b2bef..2bd0a45a 100644
--- a/port/action/schema.go
+++ b/port/action/schema.go
@@ -701,3 +701,121 @@ func validateUserInputRequiredNotSetToFalse(state *ActionModel, resp *resource.V
}
}
}
+
+var actionResourceMarkdownDescription = `
+
+# Action
+
+This resource allows you to manage self-service action.
+
+See the [Port documentation](https://docs.getport.io/create-self-service-experiences/) for more information about self-service actions.
+
+## Example Usage
+
+Create a blueprint and an action relating to that blueprint which triggers a github workflow:
+
+` + "```hcl" + `
+
+resource "port_blueprint" "myBlueprint" {
+ icon = "Terraform"
+ identifier = "myBlueprint"
+ title = "My Blueprint"
+ properties = {
+ number_props = {
+ "numberProp" = {
+ title = "Number Property"
+ required = false
+ }
+ }
+ }
+}
+
+resource "port_action" "myAction" {
+ title = "My Action"
+ blueprint = port_blueprint.myBlueprint.identifier
+ identifier = "myAction"
+ trigger = "CREATE"
+ required_approval = false
+ github_method = {
+ org = "your-org"
+ repo = "your-repo"
+ workflow = "your-workflow"
+ }
+ user_properties = {
+ string_props = {
+ stringValue = {
+ title = "String Value"
+ }
+ }
+ number_props = {
+ "numberProp" = {
+ title = "Number Value"
+ required = true
+ }
+ }
+ }
+}
+
+` + "```" + `
+
+Create related "parent" and "child" blueprints and a CREATE action for the child blueprint with user inputs to select entities from the parent blueprint and triggers a github workflow:
+
+` + "```hcl" + `
+
+
+resource "port_blueprint" "parent" {
+ icon = "Terraform"
+ title = "Parent"
+ identifier = "parent"
+ properties = {}
+}
+
+resource "port_blueprint" "child" {
+ icon = "Terraform"
+ title = "Child"
+ identifier = "child"
+ properties = {}
+ relations = {
+ "childOf" = {
+ title = "Child Of"
+ many = true
+ required = false
+ target = port_blueprint.parent.identifier
+ }
+ }
+}
+
+resource "port_action" "myAction" {
+ title = "My Action"
+ blueprint = port_blueprint.child.identifier
+ identifier = "myAction"
+ trigger = "CREATE"
+ required_approval = false
+ github_method = {
+ org = "your-org"
+ repo = "your-repo"
+ workflow = "your-workflow"
+ }
+ user_properties = {
+ string_props = {
+ singleParent = {
+ title = "Single Parent Entity Selection"
+ format = "entity"
+ blueprint = port_blueprint.parent.identifier
+ }
+ }
+ array_props = {
+ miltipleParents = {
+ title = "Single Parent Entity Selection"
+ string_items = {
+ format = "entity"
+ blueprint = port_blueprint.parent.identifier
+ }
+ }
+ }
+ }
+}
+
+` + "```" + `
+
+`
From 945de47e7abc3e8cf5bc524be2092b1c53b0cc43 Mon Sep 17 00:00:00 2001
From: Paz Hershberg
Date: Wed, 6 Mar 2024 13:29:08 +0200
Subject: [PATCH 13/25] generated the new action docs
---
docs/resources/port_action.md | 344 +++++++++++++++++++++-------------
port/action/schema.go | 2 +-
2 files changed, 214 insertions(+), 132 deletions(-)
diff --git a/docs/resources/port_action.md b/docs/resources/port_action.md
index c4d1bf1d..315cdcb0 100644
--- a/docs/resources/port_action.md
+++ b/docs/resources/port_action.md
@@ -3,12 +3,221 @@
page_title: "port_action Resource - terraform-provider-port-labs"
subcategory: ""
description: |-
- Action resource
+ Action
+ This resource allows you to manage self-service action.
+ See the Port documentation https://docs.getport.io/create-self-service-experiences/ for more information about self-service actions.
+ Example Usage
+ Create a blueprint and an action relating to that blueprint which triggers a github workflow:
+ ```hcl
+ resource "portblueprint" "myBlueprint" {
+ icon = "Terraform"
+ identifier = "myBlueprint"
+ title = "My Blueprint"
+ properties = {
+ numberprops = {
+ "numberProp" = {
+ title = "Number Property"
+ required = false
+ }
+ }
+ }
+ }
+ resource "portaction" "myAction" {
+ title = "My Action"
+ blueprint = portblueprint.myBlueprint.identifier
+ identifier = "myAction"
+ trigger = "CREATE"
+ requiredapproval = false
+ githubmethod = {
+ org = "your-org"
+ repo = "your-repo"
+ workflow = "your-workflow"
+ }
+ userproperties = {
+ stringprops = {
+ stringValue = {
+ title = "String Value"
+ }
+ }
+ number_props = {
+ "numberProp" = {
+ title = "Number Value"
+ required = true
+ }
+ }
+ }
+ }
+ ```
+ Create related "parent" and "child" blueprints and a CREATE action for the child blueprint with user inputs to select entities from the parent blueprint and triggers a github workflow:
+ ```hcl
+ resource "port_blueprint" "parent" {
+ icon = "Terraform"
+ title = "Parent"
+ identifier = "parent"
+ properties = {}
+ }
+ resource "portblueprint" "child" {
+ icon = "Terraform"
+ title = "Child"
+ identifier = "child"
+ properties = {}
+ relations = {
+ "childOf" = {
+ title = "Child Of"
+ many = true
+ required = false
+ target = portblueprint.parent.identifier
+ }
+ }
+ }
+ resource "portaction" "myAction" {
+ title = "My Action"
+ blueprint = portblueprint.child.identifier
+ identifier = "myAction"
+ trigger = "CREATE"
+ requiredapproval = false
+ githubmethod = {
+ org = "your-org"
+ repo = "your-repo"
+ workflow = "your-workflow"
+ }
+ userproperties = {
+ stringprops = {
+ singleParent = {
+ title = "Single Parent Entity Selection"
+ format = "entity"
+ blueprint = portblueprint.parent.identifier
+ }
+ }
+ arrayprops = {
+ miltipleParents = {
+ title = "Single Parent Entity Selection"
+ stringitems = {
+ format = "entity"
+ blueprint = portblueprint.parent.identifier
+ }
+ }
+ }
+ }
+ }
+ ```
---
# port_action (Resource)
-Action resource
+# Action
+
+This resource allows you to manage self-service action.
+
+See the [Port documentation](https://docs.getport.io/create-self-service-experiences/) for more information about self-service actions.
+
+## Example Usage
+
+Create a blueprint and an action relating to that blueprint which triggers a github workflow:
+
+```hcl
+
+resource "port_blueprint" "myBlueprint" {
+ icon = "Terraform"
+ identifier = "myBlueprint"
+ title = "My Blueprint"
+ properties = {
+ number_props = {
+ "numberProp" = {
+ title = "Number Property"
+ required = false
+ }
+ }
+ }
+}
+
+resource "port_action" "myAction" {
+ title = "My Action"
+ blueprint = port_blueprint.myBlueprint.identifier
+ identifier = "myAction"
+ trigger = "CREATE"
+ required_approval = false
+ github_method = {
+ org = "your-org"
+ repo = "your-repo"
+ workflow = "your-workflow"
+ }
+ user_properties = {
+ string_props = {
+ stringValue = {
+ title = "String Value"
+ }
+ }
+ number_props = {
+ "numberProp" = {
+ title = "Number Value"
+ required = true
+ }
+ }
+ }
+}
+
+```
+
+Create related "parent" and "child" blueprints and a CREATE action for the child blueprint with user inputs to select entities from the parent blueprint and triggers a github workflow:
+
+```hcl
+
+
+resource "port_blueprint" "parent" {
+ icon = "Terraform"
+ title = "Parent"
+ identifier = "parent"
+ properties = {}
+}
+
+resource "port_blueprint" "child" {
+ icon = "Terraform"
+ title = "Child"
+ identifier = "child"
+ properties = {}
+ relations = {
+ "childOf" = {
+ title = "Child Of"
+ many = true
+ required = false
+ target = port_blueprint.parent.identifier
+ }
+ }
+}
+
+resource "port_action" "myAction" {
+ title = "My Action"
+ blueprint = port_blueprint.child.identifier
+ identifier = "myAction"
+ trigger = "CREATE"
+ required_approval = false
+ github_method = {
+ org = "your-org"
+ repo = "your-repo"
+ workflow = "your-workflow"
+ }
+ user_properties = {
+ string_props = {
+ singleParent = {
+ title = "Single Parent Entity Selection"
+ format = "entity"
+ blueprint = port_blueprint.parent.identifier
+ }
+ }
+ array_props = {
+ miltipleParents = {
+ title = "Single Parent Entity Selection"
+ string_items = {
+ format = "entity"
+ blueprint = port_blueprint.parent.identifier
+ }
+ }
+ }
+ }
+}
+
+```
@@ -126,7 +335,6 @@ Optional:
Optional:
- `boolean_items` (Attributes) The items of the array property (see [below for nested schema](#nestedatt--user_properties--array_props--boolean_items))
-- `dataset` (Attributes) The dataset of the property (see [below for nested schema](#nestedatt--user_properties--array_props--dataset))
- `default_jq_query` (String) The default jq query of the array property
- `depends_on` (List of String) The properties that this property depends on
- `description` (String) The description of the property
@@ -149,37 +357,6 @@ Optional:
- `default` (List of Boolean) The default of the items
-
-### Nested Schema for `user_properties.array_props.dataset`
-
-Required:
-
-- `combinator` (String) The combinator of the dataset
-- `rules` (Attributes List) The rules of the dataset (see [below for nested schema](#nestedatt--user_properties--array_props--dataset--rules))
-
-
-### Nested Schema for `user_properties.array_props.dataset.rules`
-
-Required:
-
-- `operator` (String) The operator of the rule
-- `value` (Object) The value of the rule (see [below for nested schema](#nestedatt--user_properties--array_props--dataset--rules--value))
-
-Optional:
-
-- `blueprint` (String) The blueprint identifier of the rule
-- `property` (String) The property identifier of the rule
-
-
-### Nested Schema for `user_properties.array_props.dataset.rules.value`
-
-Optional:
-
-- `jq_query` (String)
-
-
-
-
### Nested Schema for `user_properties.array_props.number_items`
@@ -204,6 +381,7 @@ Optional:
Optional:
- `blueprint` (String) The blueprint identifier the property relates to
+- `dataset` (String) The dataset of an the entity-format items
- `default` (List of String) The default of the items
- `enum` (List of String) The enum of the items
- `enum_jq_query` (String) The enum jq query of the string items
@@ -216,7 +394,6 @@ Optional:
Optional:
-- `dataset` (Attributes) The dataset of the property (see [below for nested schema](#nestedatt--user_properties--boolean_props--dataset))
- `default` (Boolean) The default of the boolean property
- `default_jq_query` (String) The default jq query of the boolean property
- `depends_on` (List of String) The properties that this property depends on
@@ -227,44 +404,12 @@ Optional:
- `visible` (Boolean) The visibility of the boolean property
- `visible_jq_query` (String) The visibility condition jq query of the boolean property
-
-### Nested Schema for `user_properties.boolean_props.dataset`
-
-Required:
-
-- `combinator` (String) The combinator of the dataset
-- `rules` (Attributes List) The rules of the dataset (see [below for nested schema](#nestedatt--user_properties--boolean_props--dataset--rules))
-
-
-### Nested Schema for `user_properties.boolean_props.dataset.rules`
-
-Required:
-
-- `operator` (String) The operator of the rule
-- `value` (Object) The value of the rule (see [below for nested schema](#nestedatt--user_properties--boolean_props--dataset--rules--value))
-
-Optional:
-
-- `blueprint` (String) The blueprint identifier of the rule
-- `property` (String) The property identifier of the rule
-
-
-### Nested Schema for `user_properties.boolean_props.dataset.rules.value`
-
-Optional:
-
-- `jq_query` (String)
-
-
-
-
### Nested Schema for `user_properties.number_props`
Optional:
-- `dataset` (Attributes) The dataset of the property (see [below for nested schema](#nestedatt--user_properties--number_props--dataset))
- `default` (Number) The default of the number property
- `default_jq_query` (String) The default jq query of the number property
- `depends_on` (List of String) The properties that this property depends on
@@ -279,44 +424,12 @@ Optional:
- `visible` (Boolean) The visibility of the number property
- `visible_jq_query` (String) The visibility condition jq query of the number property
-
-### Nested Schema for `user_properties.number_props.dataset`
-
-Required:
-
-- `combinator` (String) The combinator of the dataset
-- `rules` (Attributes List) The rules of the dataset (see [below for nested schema](#nestedatt--user_properties--number_props--dataset--rules))
-
-
-### Nested Schema for `user_properties.number_props.dataset.rules`
-
-Required:
-
-- `operator` (String) The operator of the rule
-- `value` (Object) The value of the rule (see [below for nested schema](#nestedatt--user_properties--number_props--dataset--rules--value))
-
-Optional:
-
-- `blueprint` (String) The blueprint identifier of the rule
-- `property` (String) The property identifier of the rule
-
-
-### Nested Schema for `user_properties.number_props.dataset.rules.value`
-
-Optional:
-
-- `jq_query` (String)
-
-
-
-
### Nested Schema for `user_properties.object_props`
Optional:
-- `dataset` (Attributes) The dataset of the property (see [below for nested schema](#nestedatt--user_properties--object_props--dataset))
- `default` (String) The default of the object property
- `default_jq_query` (String) The default jq query of the object property
- `depends_on` (List of String) The properties that this property depends on
@@ -328,37 +441,6 @@ Optional:
- `visible` (Boolean) The visibility of the object property
- `visible_jq_query` (String) The visibility condition jq query of the object property
-
-### Nested Schema for `user_properties.object_props.dataset`
-
-Required:
-
-- `combinator` (String) The combinator of the dataset
-- `rules` (Attributes List) The rules of the dataset (see [below for nested schema](#nestedatt--user_properties--object_props--dataset--rules))
-
-
-### Nested Schema for `user_properties.object_props.dataset.rules`
-
-Required:
-
-- `operator` (String) The operator of the rule
-- `value` (Object) The value of the rule (see [below for nested schema](#nestedatt--user_properties--object_props--dataset--rules--value))
-
-Optional:
-
-- `blueprint` (String) The blueprint identifier of the rule
-- `property` (String) The property identifier of the rule
-
-
-### Nested Schema for `user_properties.object_props.dataset.rules.value`
-
-Optional:
-
-- `jq_query` (String)
-
-
-
-
### Nested Schema for `user_properties.string_props`
@@ -366,7 +448,7 @@ Optional:
Optional:
- `blueprint` (String) The blueprint identifier the string property relates to
-- `dataset` (Attributes) The dataset of the property (see [below for nested schema](#nestedatt--user_properties--string_props--dataset))
+- `dataset` (Attributes) The dataset of an the entity-format property (see [below for nested schema](#nestedatt--user_properties--string_props--dataset))
- `default` (String) The default of the string property
- `default_jq_query` (String) The default jq query of the string property
- `depends_on` (List of String) The properties that this property depends on
diff --git a/port/action/schema.go b/port/action/schema.go
index 2bd0a45a..fb110d03 100644
--- a/port/action/schema.go
+++ b/port/action/schema.go
@@ -645,7 +645,7 @@ func ArrayPropertySchema() schema.Attribute {
func (r *ActionResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
- MarkdownDescription: "Action resource",
+ MarkdownDescription: actionResourceMarkdownDescription,
Attributes: ActionSchema(),
}
}
From 2fe6f29bc07d256423aec6bcd414b45b7ecfa8fc Mon Sep 17 00:00:00 2001
From: Paz Hershberg
Date: Wed, 6 Mar 2024 13:55:38 +0200
Subject: [PATCH 14/25] added example to action usage with dataset in the docs
---
docs/resources/port_action.md | 155 ++++++++++++++++++++++++++++++++++
port/action/schema.go | 83 ++++++++++++++++++
2 files changed, 238 insertions(+)
diff --git a/docs/resources/port_action.md b/docs/resources/port_action.md
index 315cdcb0..b1daa57c 100644
--- a/docs/resources/port_action.md
+++ b/docs/resources/port_action.md
@@ -101,6 +101,80 @@ description: |-
}
}
```
+ Create the same resources as in the previous example, but the action's entity selection properties will only allow entities which pass the datasets:
+ ```hcl
+ resource "port_blueprint" "parent" {
+ icon = "Terraform"
+ title = "Parent"
+ identifier = "parent"
+ properties = {}
+ }
+ resource "portblueprint" "child" {
+ icon = "Terraform"
+ title = "Child"
+ identifier = "child"
+ properties = {}
+ relations = {
+ "childOf" = {
+ title = "Child Of"
+ many = true
+ required = false
+ target = portblueprint.parent.identifier
+ }
+ }
+ }
+ resource "portaction" "myAction" {
+ title = "My Action"
+ blueprint = portblueprint.child.identifier
+ identifier = "myAction"
+ trigger = "CREATE"
+ requiredapproval = false
+ githubmethod = {
+ org = "your-org"
+ repo = "your-repo"
+ workflow = "your-workflow"
+ omitpayload = true
+ omituserinputs = true
+ reportworkflowstatus = true
+ }
+ userproperties = {
+ stringprops = {
+ singleParent = {
+ title = "Single Parent Entity Selection"
+ format = "entity"
+ blueprint = portblueprint.parent.identifier
+ dataset = {
+ combinator = "and"
+ rules = [{
+ property = "$title"
+ operator = "contains"
+ value = {
+ jqquery = "\"specificValue\""
+ }
+ }]
+ }
+ }
+ }
+ arrayprops = {
+ miltipleParents = {
+ title = "Single Parent Entity Selection"
+ stringitems = {
+ format = "entity"
+ blueprint = portblueprint.parent.identifier
+ dataset = jsonencode({
+ combinator = "and"
+ rules = [{
+ property = "$title"
+ operator = "contains"
+ value = "specificValue"
+ }]
+ })
+ }
+ }
+ }
+ }
+ }
+ ```
---
# port_action (Resource)
@@ -220,6 +294,87 @@ resource "port_action" "myAction" {
```
+Create the same resources as in the previous example, but the action's entity selection properties will only allow entities which pass the `dataset`s:
+
+```hcl
+
+resource "port_blueprint" "parent" {
+ icon = "Terraform"
+ title = "Parent"
+ identifier = "parent"
+ properties = {}
+}
+
+resource "port_blueprint" "child" {
+ icon = "Terraform"
+ title = "Child"
+ identifier = "child"
+ properties = {}
+ relations = {
+ "childOf" = {
+ title = "Child Of"
+ many = true
+ required = false
+ target = port_blueprint.parent.identifier
+ }
+ }
+}
+
+resource "port_action" "myAction" {
+ title = "My Action"
+ blueprint = port_blueprint.child.identifier
+ identifier = "myAction"
+ trigger = "CREATE"
+ required_approval = false
+ github_method = {
+ org = "your-org"
+ repo = "your-repo"
+ workflow = "your-workflow"
+ omit_payload = true
+ omit_user_inputs = true
+ report_workflow_status = true
+ }
+ user_properties = {
+ string_props = {
+ singleParent = {
+ title = "Single Parent Entity Selection"
+ format = "entity"
+ blueprint = port_blueprint.parent.identifier
+ dataset = {
+ combinator = "and"
+ rules = [{
+ property = "$title"
+ operator = "contains"
+ value = {
+ jq_query = "\"specificValue\""
+ }
+ }]
+ }
+ }
+ }
+ array_props = {
+ miltipleParents = {
+ title = "Single Parent Entity Selection"
+ string_items = {
+ format = "entity"
+ blueprint = port_blueprint.parent.identifier
+ dataset = jsonencode({
+ combinator = "and"
+ rules = [{
+ property = "$title"
+ operator = "contains"
+ value = "specificValue"
+ }]
+ })
+ }
+ }
+ }
+ }
+}
+
+```
+
+
## Schema
diff --git a/port/action/schema.go b/port/action/schema.go
index fb110d03..348157c6 100644
--- a/port/action/schema.go
+++ b/port/action/schema.go
@@ -818,4 +818,87 @@ resource "port_action" "myAction" {
` + "```" + `
+
+Create the same resources as in the previous example, but the action's entity selection properties will only allow entities which pass the ` + "`dataset`s" + `:
+
+` + "```hcl" + `
+
+resource "port_blueprint" "parent" {
+ icon = "Terraform"
+ title = "Parent"
+ identifier = "parent"
+ properties = {}
+}
+
+resource "port_blueprint" "child" {
+ icon = "Terraform"
+ title = "Child"
+ identifier = "child"
+ properties = {}
+ relations = {
+ "childOf" = {
+ title = "Child Of"
+ many = true
+ required = false
+ target = port_blueprint.parent.identifier
+ }
+ }
+}
+
+resource "port_action" "myAction" {
+ title = "My Action"
+ blueprint = port_blueprint.child.identifier
+ identifier = "myAction"
+ trigger = "CREATE"
+ required_approval = false
+ github_method = {
+ org = "your-org"
+ repo = "your-repo"
+ workflow = "your-workflow"
+ omit_payload = true
+ omit_user_inputs = true
+ report_workflow_status = true
+ }
+ user_properties = {
+ string_props = {
+ singleParent = {
+ title = "Single Parent Entity Selection"
+ format = "entity"
+ blueprint = port_blueprint.parent.identifier
+ dataset = {
+ combinator = "and"
+ rules = [{
+ property = "$title"
+ operator = "contains"
+ value = {
+ jq_query = "\"specificValue\""
+ }
+ }]
+ }
+ }
+ }
+ array_props = {
+ miltipleParents = {
+ title = "Single Parent Entity Selection"
+ string_items = {
+ format = "entity"
+ blueprint = port_blueprint.parent.identifier
+ dataset = jsonencode({
+ combinator = "and"
+ rules = [{
+ property = "$title"
+ operator = "contains"
+ value = "specificValue"
+ }]
+ })
+ }
+ }
+ }
+ }
+}
+
+` + "```" + `
+
+
+
`
From e9cf5d5191f45eac61dba58369b11f8032ea6189 Mon Sep 17 00:00:00 2001
From: pazhersh <132351677+pazhersh@users.noreply.github.com>
Date: Wed, 20 Mar 2024 11:00:39 +0200
Subject: [PATCH 15/25]
PORT-7217-bug-terraform-errors-when-trying-to-conditionally-set-variable-properties
(#120)
* added models for validating the action schema
* changed the validation logic for the action schema to to parse types differently with the new validation model (to fix a bug where the types convetion failed)
* updated the validation for action schema's required-user-property attribute to work with the new typing
* added tests for the validation in the use-case that caused the bug mentioned in point 2
---
port/action/model.go | 158 +++++++++++++++++++
port/action/resource_test.go | 297 +++++++++++++++++++++++++++++++++++
port/action/schema.go | 192 +++++++++++++++++++---
3 files changed, 623 insertions(+), 24 deletions(-)
diff --git a/port/action/model.go b/port/action/model.go
index 5123375b..eeeeba38 100644
--- a/port/action/model.go
+++ b/port/action/model.go
@@ -2,6 +2,7 @@ package action
import (
"github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-go/tftypes"
)
type WebhookMethodModel struct {
@@ -70,6 +71,141 @@ type StringPropModel struct {
Encryption types.String `tfsdk:"encryption"`
}
+// StringPropValidationModel is a model used for the validation of StringPropModel resources
+type StringPropValidationModel struct {
+ Title string
+ Required *bool
+}
+
+func (e *StringPropValidationModel) FromTerraform5Value(val tftypes.Value) error {
+ v := map[string]tftypes.Value{}
+
+ err := val.As(&v)
+ if err != nil {
+ return err
+ }
+
+ err = v["title"].As(&e.Title)
+ if err != nil {
+ return err
+ }
+
+ err = v["required"].As(&e.Required)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// NumberPropValidationModel is a model used for the validation of StringPropModel resources
+type NumberPropValidationModel struct {
+ Title string
+ Required *bool
+}
+
+func (e *NumberPropValidationModel) FromTerraform5Value(val tftypes.Value) error {
+ v := map[string]tftypes.Value{}
+
+ err := val.As(&v)
+ if err != nil {
+ return err
+ }
+
+ err = v["title"].As(&e.Title)
+ if err != nil {
+ return err
+ }
+
+ err = v["required"].As(&e.Required)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// BooleanPropValidationModel is a model used for the validation of StringPropModel resources
+type BooleanPropValidationModel struct {
+ Title string
+ Required *bool
+}
+
+func (e *BooleanPropValidationModel) FromTerraform5Value(val tftypes.Value) error {
+ v := map[string]tftypes.Value{}
+
+ err := val.As(&v)
+ if err != nil {
+ return err
+ }
+
+ err = v["title"].As(&e.Title)
+ if err != nil {
+ return err
+ }
+
+ err = v["required"].As(&e.Required)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// ObjectPropValidationModel is a model used for the validation of StringPropModel resources
+type ObjectPropValidationModel struct {
+ Title string
+ Required *bool
+}
+
+func (e *ObjectPropValidationModel) FromTerraform5Value(val tftypes.Value) error {
+ v := map[string]tftypes.Value{}
+
+ err := val.As(&v)
+ if err != nil {
+ return err
+ }
+
+ err = v["title"].As(&e.Title)
+ if err != nil {
+ return err
+ }
+
+ err = v["required"].As(&e.Required)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// ArrayPropValidationModel is a model used for the validation of StringPropModel resources
+type ArrayPropValidationModel struct {
+ Title string
+ Required *bool
+}
+
+func (e *ArrayPropValidationModel) FromTerraform5Value(val tftypes.Value) error {
+ v := map[string]tftypes.Value{}
+
+ err := val.As(&v)
+ if err != nil {
+ return err
+ }
+
+ err = v["title"].As(&e.Title)
+ if err != nil {
+ return err
+ }
+
+ err = v["required"].As(&e.Required)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
type NumberPropModel struct {
Title types.String `tfsdk:"title"`
Icon types.String `tfsdk:"icon"`
@@ -188,3 +324,25 @@ type ActionModel struct {
OrderProperties types.List `tfsdk:"order_properties"`
RequiredJqQuery types.String `tfsdk:"required_jq_query"`
}
+
+// ActionValidationModel is a model used for the validation of ActionModel resources
+type ActionValidationModel struct {
+ ID types.String `tfsdk:"id"`
+ Identifier types.String `tfsdk:"identifier"`
+ Blueprint types.String `tfsdk:"blueprint"`
+ Title types.String `tfsdk:"title"`
+ Icon types.String `tfsdk:"icon"`
+ Description types.String `tfsdk:"description"`
+ RequiredApproval types.Bool `tfsdk:"required_approval"`
+ Trigger types.String `tfsdk:"trigger"`
+ KafkaMethod types.Object `tfsdk:"kafka_method"`
+ WebhookMethod types.Object `tfsdk:"webhook_method"`
+ GithubMethod types.Object `tfsdk:"github_method"`
+ AzureMethod types.Object `tfsdk:"azure_method"`
+ GitlabMethod types.Object `tfsdk:"gitlab_method"`
+ UserProperties types.Object `tfsdk:"user_properties"`
+ ApprovalWebhookNotification types.Object `tfsdk:"approval_webhook_notification"`
+ ApprovalEmailNotification types.Object `tfsdk:"approval_email_notification"`
+ OrderProperties types.List `tfsdk:"order_properties"`
+ RequiredJqQuery types.String `tfsdk:"required_jq_query"`
+}
diff --git a/port/action/resource_test.go b/port/action/resource_test.go
index 8ff6a39c..d1c18622 100644
--- a/port/action/resource_test.go
+++ b/port/action/resource_test.go
@@ -1334,3 +1334,300 @@ func TestAccPortEmailApproval(t *testing.T) {
},
})
}
+
+func TestAccPortActionStringGitlabMethodSetConditionally(t *testing.T) {
+ identifier := utils.GenID()
+ actionIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
+resource "port_action" "action1" {
+ title = "Action 1"
+ blueprint = port_blueprint.microservice.id
+ identifier = "%s"
+ trigger = "CREATE"
+ required_approval = false
+ webhook_method = port_blueprint.microservice.identifier == "%s" ? {
+ url = "https://getport.io"
+ } : null
+ user_properties = {}
+}
+ `, actionIdentifier, identifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_action.action1", "title", "Action 1"),
+ resource.TestCheckResourceAttr("port_action.action1", "identifier", actionIdentifier),
+ resource.TestCheckResourceAttr("port_action.action1", "trigger", "CREATE"),
+ resource.TestCheckResourceAttr("port_action.action1", "webhook_method.url", "https://getport.io"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortActionStringUserPropertiesConditional(t *testing.T) {
+ identifier := utils.GenID()
+ actionIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
+resource "port_action" "action1" {
+ title = "Action 1"
+ blueprint = port_blueprint.microservice.id
+ identifier = "%s"
+ trigger = "CREATE"
+ required_approval = false
+ kafka_method = {}
+ user_properties = {
+ string_props = port_blueprint.microservice.identifier == "%s" ? {
+ strProp = {
+ title = "Prop"
+ }
+ } : null
+ }
+}
+ `, actionIdentifier, identifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_action.action1", "title", "Action 1"),
+ resource.TestCheckResourceAttr("port_action.action1", "identifier", actionIdentifier),
+ resource.TestCheckResourceAttr("port_action.action1", "trigger", "CREATE"),
+ resource.TestCheckResourceAttr("port_action.action1", "user_properties.string_props.strProp.title", "Prop"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortActionNumberUserPropertiesConditional(t *testing.T) {
+ identifier := utils.GenID()
+ actionIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
+resource "port_action" "action1" {
+ title = "Action 1"
+ blueprint = port_blueprint.microservice.id
+ identifier = "%s"
+ trigger = "CREATE"
+ required_approval = false
+ kafka_method = {}
+ user_properties = {
+ number_props = port_blueprint.microservice.identifier == "%s" ? {
+ numProp = {
+ title = "Prop"
+ }
+ } : null
+ }
+}
+ `, actionIdentifier, identifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_action.action1", "title", "Action 1"),
+ resource.TestCheckResourceAttr("port_action.action1", "identifier", actionIdentifier),
+ resource.TestCheckResourceAttr("port_action.action1", "trigger", "CREATE"),
+ resource.TestCheckResourceAttr("port_action.action1", "user_properties.number_props.numProp.title", "Prop"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortActionBoolUserPropertiesConditional(t *testing.T) {
+ identifier := utils.GenID()
+ actionIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
+resource "port_action" "action1" {
+ title = "Action 1"
+ blueprint = port_blueprint.microservice.id
+ identifier = "%s"
+ trigger = "CREATE"
+ required_approval = false
+ kafka_method = {}
+ user_properties = {
+ boolean_props = port_blueprint.microservice.identifier == "%s" ? {
+ boolProp = {
+ title = "Prop"
+ }
+ } : null
+ }
+}
+ `, actionIdentifier, identifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_action.action1", "title", "Action 1"),
+ resource.TestCheckResourceAttr("port_action.action1", "identifier", actionIdentifier),
+ resource.TestCheckResourceAttr("port_action.action1", "trigger", "CREATE"),
+ resource.TestCheckResourceAttr("port_action.action1", "user_properties.boolean_props.boolProp.title", "Prop"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortActionObjectUserPropertiesConditional(t *testing.T) {
+ identifier := utils.GenID()
+ actionIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
+resource "port_action" "action1" {
+ title = "Action 1"
+ blueprint = port_blueprint.microservice.id
+ identifier = "%s"
+ trigger = "CREATE"
+ required_approval = false
+ kafka_method = {}
+ user_properties = {
+ object_props = port_blueprint.microservice.identifier == "%s" ? {
+ objProp = {
+ title = "Prop"
+ }
+ } : null
+ }
+}
+ `, actionIdentifier, identifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_action.action1", "title", "Action 1"),
+ resource.TestCheckResourceAttr("port_action.action1", "identifier", actionIdentifier),
+ resource.TestCheckResourceAttr("port_action.action1", "trigger", "CREATE"),
+ resource.TestCheckResourceAttr("port_action.action1", "user_properties.object_props.objProp.title", "Prop"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortActionArrayUserPropertiesConditional(t *testing.T) {
+ identifier := utils.GenID()
+ actionIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
+resource "port_action" "action1" {
+ title = "Action 1"
+ blueprint = port_blueprint.microservice.id
+ identifier = "%s"
+ trigger = "CREATE"
+ required_approval = false
+ kafka_method = {}
+ user_properties = {
+ array_props = port_blueprint.microservice.identifier == "%s" ? {
+ arrProp = {
+ title = "Prop"
+ }
+ } : null
+ }
+}
+ `, actionIdentifier, identifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_action.action1", "title", "Action 1"),
+ resource.TestCheckResourceAttr("port_action.action1", "identifier", actionIdentifier),
+ resource.TestCheckResourceAttr("port_action.action1", "trigger", "CREATE"),
+ resource.TestCheckResourceAttr("port_action.action1", "user_properties.array_props.arrProp.title", "Prop"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortActionNoUserPropertiesConditional(t *testing.T) {
+ identifier := utils.GenID()
+ actionIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
+resource "port_action" "action1" {
+ title = "Action 1"
+ blueprint = port_blueprint.microservice.id
+ identifier = "%s"
+ trigger = "CREATE"
+ required_approval = false
+ kafka_method = {}
+ user_properties = {
+ string_props = port_blueprint.microservice.identifier == "notTheRealIdentifier" ? {
+ strProp = {
+ title = "Prop"
+ }
+ } : null
+
+ number_props = port_blueprint.microservice.identifier == "notTheRealIdentifier" ? {
+ numProp = {
+ title = "Prop"
+ }
+ } : null
+
+ boolean_props = port_blueprint.microservice.identifier == "notTheRealIdentifier" ? {
+ boolProp = {
+ title = "Prop"
+ }
+ } : null
+
+ object_props = port_blueprint.microservice.identifier == "notTheRealIdentifier" ? {
+ objProp = {
+ title = "Prop"
+ }
+ } : null
+
+ array_props = port_blueprint.microservice.identifier == "notTheRealIdentifier" ? {
+ arrProp = {
+ title = "Prop"
+ }
+ } : null
+ }
+}
+ `, actionIdentifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_action.action1", "title", "Action 1"),
+ resource.TestCheckResourceAttr("port_action.action1", "identifier", actionIdentifier),
+ resource.TestCheckResourceAttr("port_action.action1", "trigger", "CREATE"),
+ resource.TestCheckNoResourceAttr("port_action.action1", "user_properties.string_props"),
+ resource.TestCheckNoResourceAttr("port_action.action1", "user_properties.number_props"),
+ resource.TestCheckNoResourceAttr("port_action.action1", "user_properties.boolean_props"),
+ resource.TestCheckNoResourceAttr("port_action.action1", "user_properties.object_props"),
+ resource.TestCheckNoResourceAttr("port_action.action1", "user_properties.array_props"),
+ ),
+ },
+ },
+ })
+}
diff --git a/port/action/schema.go b/port/action/schema.go
index 348157c6..ecb45604 100644
--- a/port/action/schema.go
+++ b/port/action/schema.go
@@ -15,6 +15,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/port-labs/terraform-provider-port-labs/internal/utils"
)
@@ -651,7 +652,7 @@ func (r *ActionResource) Schema(ctx context.Context, req resource.SchemaRequest,
}
func (r *ActionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
- var state *ActionModel
+ var state *ActionValidationModel
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
@@ -659,45 +660,188 @@ func (r *ActionResource) ValidateConfig(ctx context.Context, req resource.Valida
return
}
- validateUserInputRequiredNotSetToFalse(state, resp)
+ validateUserInputRequiredNotSetToFalse(ctx, state, resp)
}
-func validateUserInputRequiredNotSetToFalse(state *ActionModel, resp *resource.ValidateConfigResponse) {
- // go over all the properties and check if required is set to false, is its false, raise an error that false is not
+func validateUserInputRequiredNotSetToFalse(ctx context.Context, state *ActionValidationModel, resp *resource.ValidateConfigResponse) {
+ // go over all the properties and check if required is set to false, it is false, raise an error that false is not
// supported anymore
const errorString = "required is set to false, this is not supported anymore, if you don't want to make the stringProp required, remove the required stringProp"
- if state.UserProperties == nil {
+ if state.UserProperties.IsNull() {
return
}
- for _, stringProp := range state.UserProperties.StringProps {
- if !stringProp.Required.IsNull() && !stringProp.Required.ValueBool() && stringProp.Required == types.BoolValue(false) {
- resp.Diagnostics.AddError(errorString, fmt.Sprint(`Error in User Property: `, stringProp.Title, ` in action: `, state.Identifier))
+ var userProperties = state.UserProperties.Attributes()
+ if userProperties != nil {
+ var stringProperties, _ = userProperties["string_props"]
+
+ if stringProperties != nil {
+ var val, err = stringProperties.ToTerraformValue(ctx)
+ if err != nil {
+ return
+ }
+
+ v := map[string]tftypes.Value{}
+
+ err = val.As(&v)
+ if err != nil {
+ return
+ }
+
+ stringPropValidationsObjects := make(map[string]StringPropValidationModel, len(v))
+ for key := range v {
+ var val StringPropValidationModel
+ err = v[key].As(&val)
+
+ if err != nil {
+ return
+ }
+
+ stringPropValidationsObjects[key] = val
+ }
+
+ for _, stringProp := range stringPropValidationsObjects {
+ if stringProp.Required != nil && !*stringProp.Required {
+ resp.Diagnostics.AddError(errorString, fmt.Sprint(`Error in User Property: `, stringProp.Title, ` in action: `, state.Identifier))
+ }
+ }
}
- }
- for _, numberProp := range state.UserProperties.NumberProps {
- if !numberProp.Required.IsNull() && !numberProp.Required.ValueBool() && numberProp.Required == types.BoolValue(false) {
- resp.Diagnostics.AddError(errorString, fmt.Sprint(`Error in User Property: `, numberProp.Title, ` in action: `, state.Identifier))
+ var numberProperties, _ = userProperties["number_props"]
+
+ if numberProperties != nil {
+ var val, err = numberProperties.ToTerraformValue(ctx)
+ if err != nil {
+ return
+ }
+
+ v := map[string]tftypes.Value{}
+
+ err = val.As(&v)
+ if err != nil {
+ return
+ }
+
+ numberPropValidationsObjects := make(map[string]NumberPropValidationModel, len(v))
+ for key := range v {
+ var val NumberPropValidationModel
+ err = v[key].As(&val)
+
+ if err != nil {
+ return
+ }
+
+ numberPropValidationsObjects[key] = val
+ }
+
+ for _, numberProp := range numberPropValidationsObjects {
+ if numberProp.Required != nil && !*numberProp.Required {
+ resp.Diagnostics.AddError(errorString, fmt.Sprint(`Error in User Property: `, numberProp.Title, ` in action: `, state.Identifier))
+ }
+ }
}
- }
- for _, boolProp := range state.UserProperties.BooleanProps {
- if !boolProp.Required.IsNull() && !boolProp.Required.ValueBool() && boolProp.Required == types.BoolValue(false) {
- resp.Diagnostics.AddError(errorString, fmt.Sprint(`Error in User Property: `, boolProp.Title, ` in action: `, state.Identifier))
+ var booleanProperties, _ = userProperties["boolean_props"]
+
+ if booleanProperties != nil {
+ var val, err = booleanProperties.ToTerraformValue(ctx)
+ if err != nil {
+ return
+ }
+
+ v := map[string]tftypes.Value{}
+
+ err = val.As(&v)
+ if err != nil {
+ return
+ }
+
+ booleanPropValidationsObjects := make(map[string]BooleanPropValidationModel, len(v))
+ for key := range v {
+ var val BooleanPropValidationModel
+ err = v[key].As(&val)
+
+ if err != nil {
+ return
+ }
+
+ booleanPropValidationsObjects[key] = val
+ }
+
+ for _, booleanProp := range booleanPropValidationsObjects {
+ if booleanProp.Required != nil && !*booleanProp.Required {
+ resp.Diagnostics.AddError(errorString, fmt.Sprint(`Error in User Property: `, booleanProp.Title, ` in action: `, state.Identifier))
+ }
+ }
}
- }
- for _, objectProp := range state.UserProperties.ObjectProps {
- if !objectProp.Required.IsNull() && !objectProp.Required.ValueBool() && objectProp.Required == types.BoolValue(false) {
- resp.Diagnostics.AddError(errorString, fmt.Sprint(`Error in User Property: `, objectProp.Title, ` in action: `, state.Identifier))
+ var objectProperties, _ = userProperties["object_props"]
+
+ if objectProperties != nil {
+ var val, err = objectProperties.ToTerraformValue(ctx)
+ if err != nil {
+ return
+ }
+
+ v := map[string]tftypes.Value{}
+
+ err = val.As(&v)
+ if err != nil {
+ return
+ }
+
+ objectPropValidationsObjects := make(map[string]ObjectPropValidationModel, len(v))
+ for key := range v {
+ var val ObjectPropValidationModel
+ err = v[key].As(&val)
+
+ if err != nil {
+ return
+ }
+
+ objectPropValidationsObjects[key] = val
+ }
+
+ for _, objectProp := range objectPropValidationsObjects {
+ if objectProp.Required != nil && !*objectProp.Required {
+ resp.Diagnostics.AddError(errorString, fmt.Sprint(`Error in User Property: `, objectProp.Title, ` in action: `, state.Identifier))
+ }
+ }
}
- }
- for _, arrayProp := range state.UserProperties.ArrayProps {
- if !arrayProp.Required.IsNull() && !arrayProp.Required.ValueBool() && arrayProp.Required == types.BoolValue(false) {
- resp.Diagnostics.AddError(errorString, fmt.Sprint(`Error in User Property: `, arrayProp.Title, ` in action: `, state.Identifier))
+ var arrayProperties, _ = userProperties["array_props"]
+
+ if arrayProperties != nil {
+ var val, err = arrayProperties.ToTerraformValue(ctx)
+ if err != nil {
+ return
+ }
+
+ v := map[string]tftypes.Value{}
+
+ err = val.As(&v)
+ if err != nil {
+ return
+ }
+
+ arrayPropValidationsObjects := make(map[string]ArrayPropValidationModel, len(v))
+ for key := range v {
+ var val ArrayPropValidationModel
+ err = v[key].As(&val)
+
+ if err != nil {
+ return
+ }
+
+ arrayPropValidationsObjects[key] = val
+ }
+
+ for _, arrayProp := range arrayPropValidationsObjects {
+ if arrayProp.Required != nil && !*arrayProp.Required {
+ resp.Diagnostics.AddError(errorString, fmt.Sprint(`Error in User Property: `, arrayProp.Title, ` in action: `, state.Identifier))
+ }
+ }
}
}
}
From 398bc35c81099e3d4da57af211ccad668676b1f6 Mon Sep 17 00:00:00 2001
From: Ozz Shafriri
Date: Thu, 21 Mar 2024 13:59:42 +0200
Subject: [PATCH 16/25] change logo in README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 05c934e7..e98b777e 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+
# Port Terraform Provider
From 946aee11ec6ba56a110456c03e1a3734e2047e12 Mon Sep 17 00:00:00 2001
From: Ozz Shafriri
Date: Thu, 21 Mar 2024 15:34:12 +0200
Subject: [PATCH 17/25] change logo in README (again)
---
README.md | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index e98b777e..54e42e04 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,8 @@
-
+
# Port Terraform Provider
From 9c7009f278539ffb7b46d40b2fd3a1c4872708cf Mon Sep 17 00:00:00 2001
From: MatanHeledPort <115919235+MatanHeledPort@users.noreply.github.com>
Date: Sun, 14 Apr 2024 16:52:45 +0300
Subject: [PATCH 18/25] Create dependabot.yml
---
.github/dependabot.yml | 13 +++++++++++++
1 file changed, 13 insertions(+)
create mode 100644 .github/dependabot.yml
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..8a2a974d
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: "gomod"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ open-pull-requests-limit: 5
From d8bf68e492cc7a0182abce8121306ccc127bc590 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 14 Apr 2024 13:53:20 +0000
Subject: [PATCH 19/25] Bump actions/setup-go from 3 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v5)
---
updated-dependencies:
- dependency-name: actions/setup-go
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/cd.yml | 2 +-
.github/workflows/ci.yml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index c0e5f02e..aaa4497e 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -26,7 +26,7 @@ jobs:
run: git fetch --prune --unshallow
-
name: Set up Go
- uses: actions/setup-go@v3
+ uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
cache: true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2d42e357..81389509 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- - uses: actions/setup-go@v3
+ - uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
cache: true
@@ -37,7 +37,7 @@ jobs:
go: ['1.18', '1.19']
steps:
- uses: actions/checkout@v3
- - uses: actions/setup-go@v3
+ - uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- uses: actions/cache@v3
From e64228442c454f8ed9090b70260e15e9c983f7b0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 14 Apr 2024 13:53:23 +0000
Subject: [PATCH 20/25] Bump crazy-max/ghaction-import-gpg from 5.0.0 to 6.1.0
Bumps [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) from 5.0.0 to 6.1.0.
- [Release notes](https://github.com/crazy-max/ghaction-import-gpg/releases)
- [Commits](https://github.com/crazy-max/ghaction-import-gpg/compare/v5.0.0...v6.1.0)
---
updated-dependencies:
- dependency-name: crazy-max/ghaction-import-gpg
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/cd.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index c0e5f02e..a15344fc 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -33,7 +33,7 @@ jobs:
-
name: Import GPG key
id: import_gpg
- uses: crazy-max/ghaction-import-gpg@v5.0.0
+ uses: crazy-max/ghaction-import-gpg@v6.1.0
with:
# These secrets will need to be configured for the repository:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
From 1417d011654d791575d6a27ca68abf22bdf5f83b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 14 Apr 2024 13:53:26 +0000
Subject: [PATCH 21/25] Bump golangci/golangci-lint-action from 3 to 4
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)
---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2d42e357..8db6560e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
go-version-file: 'go.mod'
cache: true
- name: golangci-lint
- uses: golangci/golangci-lint-action@v3
+ uses: golangci/golangci-lint-action@v4
with:
version: v1.48.0
- uses: hashicorp/setup-terraform@v2
From 60497314a32db6532af7b43eab870d0ea298ad20 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 14 Apr 2024 13:53:29 +0000
Subject: [PATCH 22/25] Bump hashicorp/setup-terraform from 2 to 3
Bumps [hashicorp/setup-terraform](https://github.com/hashicorp/setup-terraform) from 2 to 3.
- [Release notes](https://github.com/hashicorp/setup-terraform/releases)
- [Changelog](https://github.com/hashicorp/setup-terraform/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/setup-terraform/compare/v2...v3)
---
updated-dependencies:
- dependency-name: hashicorp/setup-terraform
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2d42e357..4f74e163 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,7 +21,7 @@ jobs:
uses: golangci/golangci-lint-action@v3
with:
version: v1.48.0
- - uses: hashicorp/setup-terraform@v2
+ - uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.2.6
terraform_wrapper: false
From f752ff68a5e1dcaadff9f69217950ba745a5626d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 14 Apr 2024 13:53:32 +0000
Subject: [PATCH 23/25] Bump actions/cache from 3 to 4
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)
---
updated-dependencies:
- dependency-name: actions/cache
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2d42e357..09f87659 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,7 +40,7 @@ jobs:
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
From 53c519c6f29c077b7280a5b6eee96eab70dd2731 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 14 Apr 2024 13:53:38 +0000
Subject: [PATCH 24/25] Bump goreleaser/goreleaser-action from 2.9.1 to 5.0.0
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 2.9.1 to 5.0.0.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v2.9.1...v5.0.0)
---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/cd.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index c0e5f02e..793f19d9 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -40,7 +40,7 @@ jobs:
passphrase: ${{ secrets.PASSPHRASE }}
-
name: Run GoReleaser
- uses: goreleaser/goreleaser-action@v2.9.1
+ uses: goreleaser/goreleaser-action@v5.0.0
with:
version: latest
args: release --rm-dist
From 037260fcb387a5b90d9d466d48328e7d11709328 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 14 Apr 2024 13:55:00 +0000
Subject: [PATCH 25/25] Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/cd.yml | 2 +-
.github/workflows/ci.yml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index aaa4497e..3962873c 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -20,7 +20,7 @@ jobs:
steps:
-
name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
-
name: Unshallow
run: git fetch --prune --unshallow
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b639f39c..d868c202 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ jobs:
name: lint
runs-on: ubuntu-20.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
@@ -36,7 +36,7 @@ jobs:
matrix:
go: ['1.18', '1.19']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}