diff --git a/docs/resources/port_scorecard.md b/docs/resources/port_scorecard.md
new file mode 100644
index 00000000..1cd17e18
--- /dev/null
+++ b/docs/resources/port_scorecard.md
@@ -0,0 +1,63 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "port_scorecard Resource - terraform-provider-port-labs"
+subcategory: ""
+description: |-
+ scorecard resource
+---
+
+# port_scorecard (Resource)
+
+scorecard resource
+
+
+
+## Schema
+
+### Required
+
+- `blueprint` (String) The blueprint of the scorecard
+- `identifier` (String) The identifier of the scorecard
+- `rules` (Attributes List) The rules of the scorecard (see [below for nested schema](#nestedatt--rules))
+- `title` (String) The title of the scorecard
+
+### Read-Only
+
+- `created_at` (String) The creation date of the scorecard
+- `created_by` (String) The creator of the scorecard
+- `id` (String) The ID of this resource.
+- `updated_at` (String) The last update date of the scorecard
+- `updated_by` (String) The last updater of the scorecard
+
+
+
+### Nested Schema for `rules`
+
+Required:
+
+- `identifier` (String) The identifier of the rule
+- `level` (String) The level of the rule
+- `query` (Attributes) The query of the rule (see [below for nested schema](#nestedatt--rules--query))
+- `title` (String) The title of the rule
+
+
+
+### Nested Schema for `rules.query`
+
+Required:
+
+- `combinator` (String) The combinator of the query
+- `conditions` (Attributes List) The conditions of the query (see [below for nested schema](#nestedatt--rules--query--conditions))
+
+
+
+### Nested Schema for `rules.query.conditions`
+
+Required:
+
+- `operator` (String) The operator of the condition
+- `property` (String) The property of the condition
+
+Optional:
+
+- `value` (String) The value of the condition
diff --git a/docs/resources/port_webhook.md b/docs/resources/port_webhook.md
index afbd2d35..0e5c06ca 100644
--- a/docs/resources/port_webhook.md
+++ b/docs/resources/port_webhook.md
@@ -32,6 +32,8 @@ Webhook resource
- `id` (String) The ID of this resource.
- `updated_at` (String) The last update date of the webhook
- `updated_by` (String) The last updater of the webhook
+- `url` (String) The url of the webhook
+- `webhook_key` (String) The webhook key of the webhook
### Nested Schema for `mappings`
diff --git a/internal/cli/models.go b/internal/cli/models.go
index a9ad5bf8..3569601c 100644
--- a/internal/cli/models.go
+++ b/internal/cli/models.go
@@ -196,6 +196,32 @@ type (
Many *bool `json:"many,omitempty"`
}
+ Scorecard struct {
+ Meta
+ Identifier string `json:"identifier,omitempty"`
+ Title string `json:"title,omitempty"`
+ Blueprint string `json:"blueprint,omitempty"`
+ Rules []Rule `json:"rules,omitempty"`
+ }
+
+ Rule struct {
+ Identifier string `json:"identifier,omitempty"`
+ Title string `json:"title,omitempty"`
+ Level string `json:"level,omitempty"`
+ Query Query `json:"query,omitempty"`
+ }
+
+ Query struct {
+ Combinator string `json:"combinator,omitempty"`
+ Conditions []Condition `json:"conditions,omitempty"`
+ }
+
+ Condition struct {
+ Property string `json:"property,omitempty"`
+ Operator string `json:"operator,omitempty"`
+ Value *string `json:"value,omitempty"`
+ }
+
Webhook struct {
Meta
Identifier string `json:"identifier,omitempty"`
@@ -240,6 +266,7 @@ type PortBody struct {
Blueprint Blueprint `json:"blueprint"`
Action Action `json:"action"`
Integration Webhook `json:"integration"`
+ Scorecard Scorecard `json:"Scorecard"`
}
type PortProviderModel struct {
@@ -248,3 +275,7 @@ type PortProviderModel struct {
Token types.String `tfsdk:"token"`
BaseUrl types.String `tfsdk:"base_url"`
}
+
+type PortBodyDelete struct {
+ Ok bool `json:"ok"`
+}
diff --git a/internal/cli/scorecard.go b/internal/cli/scorecard.go
new file mode 100644
index 00000000..333365c8
--- /dev/null
+++ b/internal/cli/scorecard.go
@@ -0,0 +1,92 @@
+package cli
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+)
+
+func (c *PortClient) ReadScorecard(ctx context.Context, blueprintID string, scorecardID string) (*Scorecard, int, error) {
+ pb := &PortBody{}
+ url := "v1/blueprints/{blueprint_identifier}/scorecards/{scorecard_identifier}"
+ resp, err := c.Client.R().
+ SetContext(ctx).
+ SetHeader("Accept", "application/json").
+ SetResult(pb).
+ SetPathParam("blueprint_identifier", blueprintID).
+ SetPathParam("scorecard_identifier", scorecardID).
+ Get(url)
+ if err != nil {
+ return nil, resp.StatusCode(), err
+ }
+ if !pb.OK {
+ return nil, resp.StatusCode(), fmt.Errorf("failed to read scorecard, got: %s", resp.Body())
+ }
+ return &pb.Scorecard, resp.StatusCode(), nil
+}
+
+func (c *PortClient) CreateScorecard(ctx context.Context, blueprintID string, scorecard *Scorecard) (*Scorecard, error) {
+ url := "v1/blueprints/{blueprint_identifier}/scorecards"
+ resp, err := c.Client.R().
+ SetBody(scorecard).
+ SetContext(ctx).
+ SetPathParam("blueprint_identifier", blueprintID).
+ 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 {
+ return nil, fmt.Errorf("failed to create scorecard, got: %s", resp.Body())
+ }
+ return &pb.Scorecard, nil
+}
+
+func (c *PortClient) UpdateScorecard(ctx context.Context, blueprintID string, scorecardId string, scorecard *Scorecard) (*Scorecard, error) {
+ url := "v1/blueprints/{blueprint_identifier}/scorecards/{scorecard_identifier}"
+ resp, err := c.Client.R().
+ SetBody(scorecard).
+ SetContext(ctx).
+ SetPathParam("blueprint_identifier", blueprintID).
+ SetPathParam("scorecard_identifier", scorecardId).
+ 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 scorecard, got: %s", resp.Body())
+ }
+ return &pb.Scorecard, nil
+}
+
+func (c *PortClient) DeleteScorecard(ctx context.Context, blueprintID string, scorecardID string) error {
+ url := "v1/blueprints/{blueprint_identifier}/scorecards/{scorecard_identifier}"
+ resp, err := c.Client.R().
+ SetContext(ctx).
+ SetHeader("Accept", "application/json").
+ SetPathParam("blueprint_identifier", blueprintID).
+ SetPathParam("scorecard_identifier", scorecardID).
+ Delete(url)
+ if err != nil {
+ return err
+ }
+ var pb PortBodyDelete
+ err = json.Unmarshal(resp.Body(), &pb)
+ if err != nil {
+ return err
+ }
+
+ if !(pb.Ok) {
+ return fmt.Errorf("failed to delete scorecard. got:\n%s", string(resp.Body()))
+ }
+ return nil
+}
diff --git a/port/action/resource.go b/port/action/resource.go
index c0450544..28cd67e4 100644
--- a/port/action/resource.go
+++ b/port/action/resource.go
@@ -56,7 +56,7 @@ func (r *ActionResource) Read(ctx context.Context, req resource.ReadRequest, res
}
blueprintIdentifier := state.Blueprint.ValueString()
- a, statusCode, err := r.portClient.ReadAction(ctx, state.Blueprint.ValueString(), state.Identifier.ValueString())
+ a, statusCode, err := r.portClient.ReadAction(ctx, blueprintIdentifier, state.Identifier.ValueString())
if err != nil {
if statusCode == 404 {
resp.State.RemoveResource(ctx)
diff --git a/port/scorecard/model.go b/port/scorecard/model.go
new file mode 100644
index 00000000..5c76783f
--- /dev/null
+++ b/port/scorecard/model.go
@@ -0,0 +1,35 @@
+package scorecard
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type Condition struct {
+ Operator types.String `tfsdk:"operator"`
+ Property types.String `tfsdk:"property"`
+ Value types.String `tfsdk:"value"`
+}
+
+type Query struct {
+ Combinator types.String `tfsdk:"combinator"`
+ Conditions []Condition `tfsdk:"conditions"`
+}
+
+type Rule struct {
+ Identifier types.String `tfsdk:"identifier"`
+ Title types.String `tfsdk:"title"`
+ Level types.String `tfsdk:"level"`
+ Query *Query `tfsdk:"query"`
+}
+
+type ScorecardModel struct {
+ ID types.String `tfsdk:"id"`
+ Identifier types.String `tfsdk:"identifier"`
+ Blueprint types.String `tfsdk:"blueprint"`
+ Title types.String `tfsdk:"title"`
+ Rules []Rule `tfsdk:"rules"`
+ 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/scorecard/refreshScorecardState.go b/port/scorecard/refreshScorecardState.go
new file mode 100644
index 00000000..45621e22
--- /dev/null
+++ b/port/scorecard/refreshScorecardState.go
@@ -0,0 +1,50 @@
+package scorecard
+
+import (
+ "context"
+ "fmt"
+
+ "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/flex"
+)
+
+func refreshScorecardState(ctx context.Context, state *ScorecardModel, s *cli.Scorecard, blueprintIdentifier string) {
+ state.ID = types.StringValue(fmt.Sprintf("%s:%s", blueprintIdentifier, s.Identifier))
+ state.Identifier = types.StringValue(s.Identifier)
+ state.Blueprint = types.StringValue(blueprintIdentifier)
+ state.Title = types.StringValue(s.Title)
+ state.CreatedAt = types.StringValue(s.CreatedAt.String())
+ state.CreatedBy = types.StringValue(s.CreatedBy)
+ state.UpdatedAt = types.StringValue(s.UpdatedAt.String())
+ state.UpdatedBy = types.StringValue(s.UpdatedBy)
+
+ stateRules := []Rule{}
+ for _, rule := range s.Rules {
+ stateRule := &Rule{
+ Title: types.StringValue(rule.Title),
+ Level: types.StringValue(rule.Level),
+ Identifier: types.StringValue(rule.Identifier),
+ }
+ stateQuery := &Query{
+ Combinator: types.StringValue(rule.Query.Combinator),
+ }
+ stateConditions := []Condition{}
+ for _, condition := range rule.Query.Conditions {
+ stateCondition := &Condition{
+ Operator: types.StringValue(condition.Operator),
+ Property: types.StringValue(condition.Property),
+ Value: flex.GoStringToFramework(condition.Value),
+ }
+ stateConditions = append(stateConditions, *stateCondition)
+ }
+ stateQuery.Conditions = stateConditions
+
+ stateRule.Query = stateQuery
+
+ stateRules = append(stateRules, *stateRule)
+ }
+
+ state.Rules = stateRules
+
+}
diff --git a/port/scorecard/resouce.go b/port/scorecard/resouce.go
new file mode 100644
index 00000000..c9c4cbe4
--- /dev/null
+++ b/port/scorecard/resouce.go
@@ -0,0 +1,149 @@
+package scorecard
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "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"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces
+var _ resource.Resource = &ScorecardResource{}
+var _ resource.ResourceWithImportState = &ScorecardResource{}
+
+func NewScorecardResource() resource.Resource {
+ return &ScorecardResource{}
+}
+
+type ScorecardResource struct {
+ portClient *cli.PortClient
+}
+
+func (r *ScorecardResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_scorecard"
+}
+
+func (r *ScorecardResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ r.portClient = req.ProviderData.(*cli.PortClient)
+}
+
+func (r *ScorecardResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state *ScorecardModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ identifier := state.Identifier.ValueString()
+ blueprintIdentifier := state.Blueprint.ValueString()
+ s, statusCode, err := r.portClient.ReadScorecard(ctx, blueprintIdentifier, identifier)
+ if err != nil {
+ if statusCode == 404 {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError("failed to read scorecard", err.Error())
+ return
+ }
+
+ refreshScorecardState(ctx, state, s, blueprintIdentifier)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ScorecardResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var state *ScorecardModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ s, err := scorecardResourceToPortBody(ctx, state)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to convert scorecard resource to body", err.Error())
+ return
+ }
+
+ sp, err := r.portClient.CreateScorecard(ctx, state.Blueprint.ValueString(), s)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to create scorecard", err.Error())
+ return
+ }
+
+ writeScorecardComputedFieldsToState(state, sp)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func writeScorecardComputedFieldsToState(state *ScorecardModel, wp *cli.Scorecard) {
+ state.ID = types.StringValue(fmt.Sprintf("%s:%s", wp.Blueprint, wp.Identifier))
+ state.Identifier = types.StringValue(wp.Identifier)
+ state.CreatedAt = types.StringValue(wp.CreatedAt.String())
+ state.CreatedBy = types.StringValue(wp.CreatedBy)
+ state.UpdatedAt = types.StringValue(wp.UpdatedAt.String())
+ state.UpdatedBy = types.StringValue(wp.UpdatedBy)
+}
+
+func (r *ScorecardResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var state *ScorecardModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ s, err := scorecardResourceToPortBody(ctx, state)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to convert scorecard resource to body", err.Error())
+ return
+ }
+
+ sp, err := r.portClient.UpdateScorecard(ctx, state.Blueprint.ValueString(), state.Identifier.ValueString(), s)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to update the scorecard", err.Error())
+ return
+ }
+
+ writeScorecardComputedFieldsToState(state, sp)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *ScorecardResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state *ScorecardModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.portClient.DeleteScorecard(ctx, state.Blueprint.ValueString(), state.Identifier.ValueString())
+
+ if err != nil {
+ resp.Diagnostics.AddError("failed to delete scorecard", err.Error())
+ return
+ }
+
+}
+
+func (r *ScorecardResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ idParts := strings.Split(req.ID, ":")
+
+ if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
+ resp.Diagnostics.AddError("invalid import ID", "import ID must be in the format :")
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("blueprint"), idParts[0])...)
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("identifier"), idParts[1])...)
+}
diff --git a/port/scorecard/resource_test.go b/port/scorecard/resource_test.go
new file mode 100644
index 00000000..ff6be6ec
--- /dev/null
+++ b/port/scorecard/resource_test.go
@@ -0,0 +1,298 @@
+package scorecard_test
+
+import (
+ "fmt"
+ "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 = {
+ "author" = {
+ type = "string"
+ title = "text"
+ }
+ "url" = {
+ type = "string"
+ title = "text"
+ }
+ }
+ }
+ }
+ `, identifier)
+}
+
+func TestAccPortScorecardBasic(t *testing.T) {
+ blueprintIdentifier := utils.GenID()
+ scorecardIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(blueprintIdentifier) + fmt.Sprintf(`
+ resource "port_scorecard" "test" {
+ identifier = "%s"
+ title = "Scorecard 1"
+ blueprint = "%s"
+ rules = [{
+ identifier = "hasTeam"
+ title = "Has Team"
+ level = "Gold"
+ query = {
+ combinator = "and"
+ conditions = [{
+ property = "$team"
+ operator = "isNotEmpty"
+ }]
+ }
+ }]
+
+ depends_on = [
+ port_blueprint.microservice
+ ]
+ }`, scorecardIdentifier, blueprintIdentifier)
+ 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_scorecard.test", "title", "Scorecard 1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "blueprint", blueprintIdentifier),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.#", "1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.identifier", "hasTeam"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.title", "Has Team"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.level", "Gold"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.combinator", "and"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.#", "1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.0.property", "$team"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.0.operator", "isNotEmpty"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortScorecard(t *testing.T) {
+ blueprintIdentifier := utils.GenID()
+ scorecardIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(blueprintIdentifier) + fmt.Sprintf(`
+ resource "port_scorecard" "test" {
+ identifier = "%s"
+ title = "Scorecard 1"
+ blueprint = "%s"
+ rules = [{
+ identifier = "test1"
+ title = "Test1"
+ level = "Gold"
+ query = {
+ combinator = "and"
+ conditions = [{
+ property = "$team"
+ operator = "isNotEmpty"
+ },
+ {
+ property = "author",
+ "operator" : "=",
+ "value" : "myValue"
+ }]
+ }
+ },
+ {
+ identifier = "test2"
+ title = "Test2"
+ level = "Silver"
+ query = {
+ combinator = "and"
+ conditions = [{
+ property = "url"
+ operator = "isNotEmpty"
+ }]
+ }
+ }]
+
+ depends_on = [
+ port_blueprint.microservice
+ ]
+ }`, scorecardIdentifier, blueprintIdentifier)
+ 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_scorecard.test", "title", "Scorecard 1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "blueprint", blueprintIdentifier),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.#", "2"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.identifier", "test1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.title", "Test1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.level", "Gold"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.combinator", "and"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.#", "2"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.0.property", "$team"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.0.operator", "isNotEmpty"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.1.property", "author"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.1.operator", "="),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.1.value", "myValue"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.1.identifier", "test2"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.1.title", "Test2"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.1.level", "Silver"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.1.query.combinator", "and"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.1.query.conditions.#", "1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.1.query.conditions.0.property", "url"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.1.query.conditions.0.operator", "isNotEmpty"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortScorecardUpdate(t *testing.T) {
+ blueprintIdentifier := utils.GenID()
+ scorecardIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(blueprintIdentifier) + fmt.Sprintf(`
+ resource "port_scorecard" "test" {
+ identifier = "%s"
+ title = "Scorecard 1"
+ blueprint = "%s"
+ rules = [{
+ identifier = "hasTeam"
+ title = "Has Team"
+ level = "Gold"
+ query = {
+ combinator = "and"
+ conditions = [{
+ property = "$team"
+ operator = "isNotEmpty"
+ }]
+ }
+ }]
+
+ depends_on = [
+ port_blueprint.microservice
+ ]
+ }`, scorecardIdentifier, blueprintIdentifier)
+
+ var testAccActionConfigUpdate = testAccCreateBlueprintConfig(blueprintIdentifier) + fmt.Sprintf(`
+ resource "port_scorecard" "test" {
+ identifier = "%s"
+ title = "Scorecard 2"
+ blueprint = "%s"
+ rules = [{
+ identifier = "hasTeam"
+ title = "Has Team"
+ level = "Bronze"
+ query = {
+ combinator = "or"
+ conditions = [{
+ property = "$team"
+ operator = "isNotEmpty"
+ }]
+ }
+ }]
+ depends_on = [
+ port_blueprint.microservice
+ ]
+ }`, scorecardIdentifier, blueprintIdentifier)
+
+ 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_scorecard.test", "title", "Scorecard 1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "blueprint", blueprintIdentifier),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.#", "1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.identifier", "hasTeam"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.title", "Has Team"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.level", "Gold"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.combinator", "and"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.#", "1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.0.property", "$team"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.0.operator", "isNotEmpty"),
+ ),
+ },
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigUpdate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_scorecard.test", "title", "Scorecard 2"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "blueprint", blueprintIdentifier),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.#", "1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.identifier", "hasTeam"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.title", "Has Team"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.level", "Bronze"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.combinator", "or"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.#", "1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.0.property", "$team"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.0.operator", "isNotEmpty"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortScorecardImport(t *testing.T) {
+ blueprintIdentifier := utils.GenID()
+ scorecardIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(blueprintIdentifier) + fmt.Sprintf(`
+ resource "port_scorecard" "test" {
+ identifier = "%s"
+ title = "Scorecard 1"
+ blueprint = "%s"
+ rules = [{
+ identifier = "hasTeam"
+ title = "Has Team"
+ level = "Gold"
+ query = {
+ combinator = "and"
+ conditions = [{
+ property = "$team"
+ operator = "isNotEmpty"
+ }]
+ }
+ }]
+
+ depends_on = [
+ port_blueprint.microservice
+ ]
+ }`, scorecardIdentifier, blueprintIdentifier)
+
+ 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_scorecard.test", "title", "Scorecard 1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "blueprint", blueprintIdentifier),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.#", "1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.identifier", "hasTeam"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.title", "Has Team"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.level", "Gold"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.combinator", "and"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.#", "1"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.0.property", "$team"),
+ resource.TestCheckResourceAttr("port_scorecard.test", "rules.0.query.conditions.0.operator", "isNotEmpty"),
+ ),
+ },
+ {
+ ResourceName: "port_scorecard.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateId: fmt.Sprintf("%s:%s", blueprintIdentifier, scorecardIdentifier),
+ },
+ },
+ })
+}
diff --git a/port/scorecard/schema.go b/port/scorecard/schema.go
new file mode 100644
index 00000000..3ce69277
--- /dev/null
+++ b/port/scorecard/schema.go
@@ -0,0 +1,124 @@
+package scorecard
+
+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"
+)
+
+func ConditionSchema() map[string]schema.Attribute {
+ return map[string]schema.Attribute{
+ "operator": schema.StringAttribute{
+ MarkdownDescription: "The operator of the condition",
+ Required: true,
+ },
+ "property": schema.StringAttribute{
+ MarkdownDescription: "The property of the condition",
+ Required: true,
+ },
+ "value": schema.StringAttribute{
+ MarkdownDescription: "The value of the condition",
+ Optional: true,
+ },
+ }
+}
+
+func RuleSchema() map[string]schema.Attribute {
+ return map[string]schema.Attribute{
+ "identifier": schema.StringAttribute{
+ MarkdownDescription: "The identifier of the rule",
+ Required: true,
+ },
+ "title": schema.StringAttribute{
+ MarkdownDescription: "The title of the rule",
+ Required: true,
+ },
+ "level": schema.StringAttribute{
+ MarkdownDescription: "The level of the rule",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("Bronze", "Silver", "Gold"),
+ },
+ },
+ "query": schema.SingleNestedAttribute{
+ MarkdownDescription: "The query of the rule",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "combinator": schema.StringAttribute{
+ MarkdownDescription: "The combinator of the query",
+ Validators: []validator.String{
+ stringvalidator.OneOf("and", "or"),
+ },
+ Required: true,
+ },
+ "conditions": schema.ListNestedAttribute{
+ MarkdownDescription: "The conditions of the query",
+ Required: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: ConditionSchema(),
+ },
+ },
+ },
+ },
+ }
+}
+func ScorecardSchema() map[string]schema.Attribute {
+ return map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "identifier": schema.StringAttribute{
+ MarkdownDescription: "The identifier of the scorecard",
+ Required: true,
+ },
+ "blueprint": schema.StringAttribute{
+ MarkdownDescription: "The blueprint of the scorecard",
+ Required: true,
+ },
+ "title": schema.StringAttribute{
+ MarkdownDescription: "The title of the scorecard",
+ Required: true,
+ },
+ "rules": schema.ListNestedAttribute{
+ MarkdownDescription: "The rules of the scorecard",
+ Required: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: RuleSchema(),
+ },
+ },
+ "created_at": schema.StringAttribute{
+ MarkdownDescription: "The creation date of the scorecard",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "created_by": schema.StringAttribute{
+ MarkdownDescription: "The creator of the scorecard",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "updated_at": schema.StringAttribute{
+ MarkdownDescription: "The last update date of the scorecard",
+ Computed: true,
+ },
+ "updated_by": schema.StringAttribute{
+ MarkdownDescription: "The last updater of the scorecard",
+ Computed: true,
+ },
+ }
+}
+
+func (r *ScorecardResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "scorecard resource",
+ Attributes: ScorecardSchema(),
+ }
+}
diff --git a/port/scorecard/scorecardkResourceToPortBody.go b/port/scorecard/scorecardkResourceToPortBody.go
new file mode 100644
index 00000000..2a3546db
--- /dev/null
+++ b/port/scorecard/scorecardkResourceToPortBody.go
@@ -0,0 +1,50 @@
+package scorecard
+
+import (
+ "context"
+
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+)
+
+func scorecardResourceToPortBody(ctx context.Context, state *ScorecardModel) (*cli.Scorecard, error) {
+ s := &cli.Scorecard{
+ Identifier: state.Identifier.ValueString(),
+ Title: state.Title.ValueString(),
+ }
+
+ rules := []cli.Rule{}
+
+ for _, stateRule := range state.Rules {
+ rule := &cli.Rule{
+ Level: stateRule.Level.ValueString(),
+ Identifier: stateRule.Identifier.ValueString(),
+ Title: stateRule.Title.ValueString(),
+ }
+
+ query := &cli.Query{
+ Combinator: stateRule.Query.Combinator.ValueString(),
+ }
+
+ conditions := []cli.Condition{}
+ for _, stateCondition := range stateRule.Query.Conditions {
+ condition := &cli.Condition{
+ Property: stateCondition.Property.ValueString(),
+ Operator: stateCondition.Operator.ValueString(),
+ }
+
+ if !stateCondition.Value.IsNull() {
+ value := stateCondition.Value.ValueString()
+ condition.Value = &value
+ }
+
+ conditions = append(conditions, *condition)
+ }
+ query.Conditions = conditions
+ rule.Query = *query
+ rules = append(rules, *rule)
+ }
+
+ s.Rules = rules
+
+ return s, nil
+}
diff --git a/provider/provider.go b/provider/provider.go
index a007ea4b..5eff2c63 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -13,6 +13,7 @@ import (
"github.com/port-labs/terraform-provider-port-labs/port/action"
"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/scorecard"
"github.com/port-labs/terraform-provider-port-labs/port/webhook"
"github.com/port-labs/terraform-provider-port-labs/version"
)
@@ -126,6 +127,7 @@ func (p *PortLabsProvider) Resources(ctx context.Context) []func() resource.Reso
entity.NewEntityResource,
action.NewActionResource,
webhook.NewWebhookResource,
+ scorecard.NewScorecardResource,
}
}