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..e02801c8 100644 --- a/docs/resources/port_blueprint.md +++ b/docs/resources/port_blueprint.md @@ -97,6 +97,11 @@ 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 such as the PORT UI or different integrations. + In this case, when trying to delete the blueprint, Terraform will fail because it will try to delete the entities that were created outside of Terraform. + To overcome this behavior, you can set the environment variable PORT_FORCE_DELETE_ENTITIES to true. + This will trigger a migration that will delete all the entities in the blueprint and then delete the blueprint itself. --- # port_blueprint (Resource) @@ -212,6 +217,14 @@ 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 such as the PORT UI or different integrations. +In this case, when trying to delete the blueprint, Terraform will fail because it will try to delete the entities that were created outside of Terraform. + +To overcome this behavior, you can set the environment variable `PORT_FORCE_DELETE_ENTITIES` to `true`. +This will trigger a migration that will delete all the entities in the blueprint and then delete the blueprint itself. + @@ -444,5 +457,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..e016ac05 100644 --- a/docs/resources/port_scorecard.md +++ b/docs/resources/port_scorecard.md @@ -3,12 +3,204 @@ 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 +239,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/resource.go b/port/blueprint/resource.go index 25c19134..b4f4ce45 100644 --- a/port/blueprint/resource.go +++ b/port/blueprint/resource.go @@ -2,6 +2,10 @@ package blueprint import ( "context" + "encoding/json" + "github.com/hashicorp/terraform-plugin-log/tflog" + "os" + "time" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -212,12 +216,53 @@ func (r *BlueprintResource) Delete(ctx context.Context, req resource.DeleteReque return } - err := r.portClient.DeleteBlueprint(ctx, state.Identifier.ValueString()) + forceDeleteEntitiesEnabled := os.Getenv("PORT_FORCE_DELETE_ENTITIES") == "true" - if err != nil { - resp.Diagnostics.AddError("failed to delete blueprint", err.Error()) - return + if !forceDeleteEntitiesEnabled { + err := r.portClient.DeleteBlueprint(ctx, state.Identifier.ValueString()) + if err != nil { + resp.Diagnostics.AddError("failed to delete blueprint", err.Error()) + return + } + } else { + migrationId, err := r.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 := r.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 + } + var migrationForLog map[string]interface{} + mm, _ := json.Marshal(migration) + err = json.Unmarshal(mm, &migrationForLog) + if err != nil { + resp.Diagnostics.AddError("failed to get migration status", err.Error()) + return + } + tflog.Info(ctx, "Waiting for delete migration to complete", migrationForLog) + time.Sleep(5 * time.Second) + } } + } func (r *BlueprintResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { diff --git a/port/blueprint/schema.go b/port/blueprint/schema.go index d5a86fae..210e7423 100644 --- a/port/blueprint/schema.go +++ b/port/blueprint/schema.go @@ -568,4 +568,14 @@ 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 environment variable ` + "`PORT_FORCE_DELETE_ENTITIES=true`" + `. +This will trigger a migration that will delete all the entities in the blueprint and then delete the blueprint itself. + +`