diff --git a/docs/data-sources/port_search.md b/docs/data-sources/port_search.md new file mode 100644 index 00000000..77e55be7 --- /dev/null +++ b/docs/data-sources/port_search.md @@ -0,0 +1,227 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "port_search Data Source - terraform-provider-port-labs" +subcategory: "" +description: |- + Search Data Source + The search data source allows you to search for entities in Port. + See the Port documentation https://docs.getport.io/search-and-query/ for more information about the search capabilities in Port. + Example Usage + Search for all entities in a specific blueprint: + ```hcl + data "portsearch" "allservice" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "Service" }, + ] + }) + } + ``` + Search for entity with specific identifier in a specific blueprint to create another resource based on the values of the entity: + ```hcl + data "portsearch" "adsservice" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "Service" }, + { "operator" : "=", "property" : "$identifier", "value" : "Ads" }, + ] + }) + } + ``` + Scorecards automation example + In this example we are creating a jira task for each service that its Ownership Scorecard hasn't reached Gold level : + ```hcl + data "portsearch" "allservices" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "microservice" }, + ] + }) + } + locals { + // Count the number of services that are not owned by a team with a Gold level + microserviceownershipwithoutgoldlevel = length([ + for entity in data.portsearch.allservices.entities : entity.scorecards["ownership"].level + if entity.scorecards["ownership"].level != "Gold" + ]) + } + // create jira issue per service that is not owned by a team with a Gold level + resource "jiraissue" "microserviceownershipwithoutgoldlevel" { + count = local.microserviceownershipwithoutgoldlevel + issuetype = "Task" + project_key = "PORT" + summary = "Service ${data.portsearch.backendservices.entities[count.index].title} hasn't reached Gold level in Ownership Scorecard" + description = "Service https://app.getport.io/${port_blueprint.microservice.identifier}Entity/${data.port_search.backend_services.entities[count.index].identifier} is not owned by a team with a Gold level, please assign a team with a Gold level to the service" + } + ``` +--- + +# port_search (Data Source) + +# Search Data Source + +The search data source allows you to search for entities in Port. + +See the [Port documentation](https://docs.getport.io/search-and-query/) for more information about the search capabilities in Port. + +## Example Usage + +### Search for all entities in a specific blueprint: + +```hcl + +data "port_search" "all_service" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "Service" }, + ] + }) +} + + +``` + +### Search for entity with specific identifier in a specific blueprint to create another resource based on the values of the entity: + + +```hcl + +data "port_search" "ads_service" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "Service" }, + { "operator" : "=", "property" : "$identifier", "value" : "Ads" }, + ] + }) +} + + +``` + +### Scorecards automation example +In this example we are creating a jira task for each service that its Ownership Scorecard hasn't reached Gold level : + +```hcl + +data "port_search" "all_services" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "microservice" }, + ] + }) +} + +locals { + // Count the number of services that are not owned by a team with a Gold level + microservice_ownership_without_gold_level = length([ + for entity in data.port_search.all_services.entities : entity.scorecards["ownership"].level + if entity.scorecards["ownership"].level != "Gold" + ]) +} + +// create jira issue per service that is not owned by a team with a Gold level +resource "jira_issue" "microservice_ownership_without_gold_level" { + count = local.microservice_ownership_without_gold_level + issue_type = "Task" + + project_key = "PORT" + + summary = "Service ${data.port_search.backend_services.entities[count.index].title} hasn't reached Gold level in Ownership Scorecard" + description = "[Service](https://app.getport.io/${port_blueprint.microservice.identifier}Entity/${data.port_search.backend_services.entities[count.index].identifier}) is not owned by a team with a Gold level, please assign a team with a Gold level to the service" +} + + +``` + + + + +## Schema + +### Required + +- `query` (String) The search query + +### Optional + +- `attach_title_to_relation` (Boolean) Attach title to relation +- `exclude` (List of String) Properties to exclude from the results +- `exclude_calculated_properties` (Boolean) Exclude calculated properties +- `include` (List of String) Properties to include in the results + +### Read-Only + +- `entities` (Attributes List) A list of entities matching the search query (see [below for nested schema](#nestedatt--entities)) +- `id` (String) The ID of this resource. +- `matching_blueprints` (List of String) The matching blueprints for the search query + + +### Nested Schema for `entities` + +Optional: + +- `icon` (String) The icon of the entity +- `properties` (Attributes) The properties of the entity (see [below for nested schema](#nestedatt--entities--properties)) +- `relations` (Attributes) The relations of the entity (see [below for nested schema](#nestedatt--entities--relations)) +- `run_id` (String) The runID of the action run that created the entity +- `scorecards` (Map of Object) The scorecards of the entity (see [below for nested schema](#nestedatt--entities--scorecards)) +- `teams` (List of String) The teams the entity belongs to +- `title` (String) The title of the entity + +Read-Only: + +- `blueprint` (String) The blueprint identifier the entity relates to +- `created_at` (String) The creation date of the entity +- `created_by` (String) The creator of the entity +- `identifier` (String) The identifier of the entity +- `updated_at` (String) The last update date of the entity +- `updated_by` (String) The last updater of the entity + + +### Nested Schema for `entities.properties` + +Optional: + +- `array_props` (Attributes) The array properties of the entity (see [below for nested schema](#nestedatt--entities--properties--array_props)) +- `boolean_props` (Map of Boolean) The bool properties of the entity +- `number_props` (Map of Number) The number properties of the entity +- `object_props` (Map of String) The object properties of the entity +- `string_props` (Map of String) The string properties of the entity + + +### Nested Schema for `entities.properties.array_props` + +Optional: + +- `boolean_items` (Map of List of Boolean) +- `number_items` (Map of List of Number) +- `object_items` (Map of List of String) +- `string_items` (Map of List of String) + + + + +### Nested Schema for `entities.relations` + +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 + + + +### Nested Schema for `entities.scorecards` + +Read-Only: + +- `level` (String) +- `rules` (List of Object) (see [below for nested schema](#nestedobjatt--entities--scorecards--rules)) + + +### Nested Schema for `entities.scorecards.rules` + +Read-Only: + +- `identifier` (String) +- `level` (String) +- `status` (String) diff --git a/internal/cli/models.go b/internal/cli/models.go index 2d223410..f5301b43 100644 --- a/internal/cli/models.go +++ b/internal/cli/models.go @@ -19,14 +19,27 @@ type ( ExpiresIn int64 `json:"expiresIn"` TokenType string `json:"tokenType"` } + + ScorecardRulesModel struct { + Identifier string `tfsdk:"identifier"` + Status string `tfsdk:"status"` + Level string `tfsdk:"level"` + } + + ScorecardModel struct { + Rules []ScorecardRulesModel `tfsdk:"rules"` + Level string `tfsdk:"level"` + } + Entity struct { Meta - Identifier string `json:"identifier,omitempty"` - Title string `json:"title"` - Blueprint string `json:"blueprint"` - Team []string `json:"team,omitempty"` - Properties map[string]any `json:"properties"` - Relations map[string]any `json:"relations"` + Identifier string `json:"identifier,omitempty"` + Title string `json:"title"` + Blueprint string `json:"blueprint"` + Team []string `json:"team,omitempty"` + Properties map[string]any `json:"properties"` + Relations map[string]any `json:"relations"` + Scorecards map[string]ScorecardModel `json:"scorecards,omitempty"` // TODO: add the rest of the fields. } @@ -385,6 +398,14 @@ type ( FailureCount int `json:"failureCount,omitempty"` SuccessCount int `json:"successCount,omitempty"` } + + SearchRequestQuery struct { + Query *map[string]any `json:"query"` + ExcludeCalculatedProperties *bool `json:"exclude_calculated_properties,omitempty"` + Include []string `json:"include,omitempty"` + Exclude []string `json:"exclude,omitempty"` + AttachTitleToRelation *bool `json:"attach_title_to_relation,omitempty"` + } ) type PortBody struct { @@ -402,6 +423,22 @@ type PortBody struct { Migration Migration `json:"migration"` } +type SearchEntityResult struct { + Meta + Identifier string `json:"identifier,omitempty"` + Title string `json:"title,omitempty"` + Icon *string `json:"icon,omitempty"` + Team []string `json:"team,omitempty"` + Properties map[string]any `json:"properties,omitempty"` + Relations map[string]any `json:"relations,omitempty"` +} + +type SearchResult struct { + OK bool `json:"ok"` + MatchingBlueprints []string `json:"matchingBlueprints"` + Entities []Entity `json:"entities"` +} + type PortPagePermissionsBody struct { OK bool `json:"ok"` PagePermissions PagePermissions `json:"permissions"` diff --git a/internal/cli/search.go b/internal/cli/search.go new file mode 100644 index 00000000..47c8292f --- /dev/null +++ b/internal/cli/search.go @@ -0,0 +1,48 @@ +package cli + +import ( + "context" + "encoding/json" + "fmt" + "strings" +) + +func (c *PortClient) Search(ctx context.Context, searchRequest *SearchRequestQuery) (*SearchResult, error) { + url := "v1/entities/search" + + req := c.Client.R(). + SetContext(ctx). + SetBody(*searchRequest.Query). + SetHeader("Accept", "application/json") + + if searchRequest.ExcludeCalculatedProperties != nil { + req.SetQueryParam("exclude_calculated_properties", fmt.Sprintf("%v", &searchRequest.ExcludeCalculatedProperties)) + } + + if searchRequest.Include != nil && len(searchRequest.Include) > 0 { + req.SetQueryParam("include", strings.Join(searchRequest.Include, ",")) + } + + if searchRequest.Exclude != nil && len(searchRequest.Exclude) > 0 { + req.SetQueryParam("exclude", strings.Join(searchRequest.Exclude, ",")) + } + + if searchRequest.AttachTitleToRelation != nil { + req.SetQueryParam("attach_title_to_relation", fmt.Sprintf("%v", &searchRequest.AttachTitleToRelation)) + } + + resp, err := req.Post(url) + + if err != nil { + return nil, err + } + var searchResult SearchResult + err = json.Unmarshal(resp.Body(), &searchResult) + if err != nil { + return nil, err + } + if !searchResult.OK { + return nil, fmt.Errorf("failed to search, got: %s", resp.Body()) + } + return &searchResult, nil +} diff --git a/port/integration/schema.go b/port/integration/schema.go index f83e56cb..663ec365 100644 --- a/port/integration/schema.go +++ b/port/integration/schema.go @@ -51,10 +51,6 @@ func IntegrationSchema() map[string]schema.Attribute { } } -func StaticString(s string) { - panic("unimplemented") -} - func (r *IntegrationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: IntegrationResourceMarkdownDescription, diff --git a/port/search/dataSource.go b/port/search/dataSource.go new file mode 100644 index 00000000..8804aefa --- /dev/null +++ b/port/search/dataSource.go @@ -0,0 +1,77 @@ +package search + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/port-labs/terraform-provider-port-labs/v2/internal/cli" +) + +var _ datasource.DataSource = &SearchDataSource{} + +func NewSearchDataSource() datasource.DataSource { + return &SearchDataSource{} +} + +type SearchDataSource struct { + portClient *cli.PortClient +} + +func (d *SearchDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + d.portClient = req.ProviderData.(*cli.PortClient) +} + +func (d *SearchDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_search" +} + +func (d *SearchDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data SearchDataModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + searchRequest, err := searchResourceToPortBody(&data) + if err != nil { + resp.Diagnostics.AddError("failed to convert search data to port body", err.Error()) + return + } + + searchResult, err := d.portClient.Search(ctx, searchRequest) + if err != nil { + resp.Diagnostics.AddError("failed to search", err.Error()) + return + } + + data.ID = types.StringValue(data.GenerateID()) + data.MatchingBlueprints = goStringListToTFList(searchResult.MatchingBlueprints) + + blueprints := make(map[string]cli.Blueprint) + for _, blueprint := range searchResult.MatchingBlueprints { + b, _, err := d.portClient.ReadBlueprint(ctx, blueprint) + if err != nil { + resp.Diagnostics.AddError("failed to read blueprint", err.Error()) + return + } + blueprints[blueprint] = *b + } + + for _, entity := range searchResult.Entities { + matchingEntityBlueprint := blueprints[entity.Blueprint] + e := refreshEntityState(ctx, &entity, &matchingEntityBlueprint) + data.Entities = append(data.Entities, *e) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func goStringListToTFList(list []string) []types.String { + var result = make([]types.String, len(list)) + for i, u := range list { + result[i] = types.StringValue(u) + } + + return result +} diff --git a/port/search/dataSourceSchema.go b/port/search/dataSourceSchema.go new file mode 100644 index 00000000..d3700305 --- /dev/null +++ b/port/search/dataSourceSchema.go @@ -0,0 +1,278 @@ +package search + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func EntitySchema() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "identifier": schema.StringAttribute{ + MarkdownDescription: "The identifier of the entity", + Computed: true, + }, + "title": schema.StringAttribute{ + MarkdownDescription: "The title of the entity", + Computed: true, + Optional: true, + }, + "icon": schema.StringAttribute{ + MarkdownDescription: "The icon of the entity", + Computed: true, + Optional: true, + }, + "run_id": schema.StringAttribute{ + MarkdownDescription: "The runID of the action run that created the entity", + Computed: true, + Optional: true, + }, + "teams": schema.ListAttribute{ + MarkdownDescription: "The teams the entity belongs to", + Computed: true, + Optional: true, + ElementType: types.StringType, + }, + "blueprint": schema.StringAttribute{ + MarkdownDescription: "The blueprint identifier the entity relates to", + Computed: true, + }, + "properties": schema.SingleNestedAttribute{ + MarkdownDescription: "The properties of the entity", + Computed: true, + Optional: true, + Attributes: map[string]schema.Attribute{ + "string_props": schema.MapAttribute{ + MarkdownDescription: "The string properties of the entity", + Computed: true, + Optional: true, + ElementType: types.StringType, + }, + "number_props": schema.MapAttribute{ + MarkdownDescription: "The number properties of the entity", + Computed: true, + Optional: true, + ElementType: types.Float64Type, + }, + "boolean_props": schema.MapAttribute{ + MarkdownDescription: "The bool properties of the entity", + Computed: true, + Optional: true, + ElementType: types.BoolType, + }, + "object_props": schema.MapAttribute{ + MarkdownDescription: "The object properties of the entity", + Computed: true, + Optional: true, + ElementType: types.StringType, + }, + "array_props": schema.SingleNestedAttribute{ + MarkdownDescription: "The array properties of the entity", + Computed: true, + Optional: true, + Attributes: map[string]schema.Attribute{ + "string_items": schema.MapAttribute{ + ElementType: types.ListType{ElemType: types.StringType}, + Computed: true, + Optional: true, + }, + "number_items": schema.MapAttribute{ + ElementType: types.ListType{ElemType: types.Float64Type}, + Computed: true, + Optional: true, + }, + "boolean_items": schema.MapAttribute{ + ElementType: types.ListType{ElemType: types.BoolType}, + Computed: true, + Optional: true, + }, + "object_items": schema.MapAttribute{ + ElementType: types.ListType{ElemType: types.StringType}, + Computed: true, + Optional: true, + }, + }, + }, + }, + }, + "scorecards": schema.MapAttribute{ + MarkdownDescription: "The scorecards of the entity", + Computed: true, + Optional: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "rules": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "identifier": types.StringType, + "status": types.StringType, + "level": types.StringType, + }, + }, + }, + "level": types.StringType, + }, + }, + }, + "relations": schema.SingleNestedAttribute{ + MarkdownDescription: "The relations of the entity", + Computed: true, + Optional: true, + Attributes: map[string]schema.Attribute{ + "single_relations": schema.MapAttribute{ + MarkdownDescription: "The single relation of the entity", + Computed: true, + Optional: true, + ElementType: types.StringType, + }, + "many_relations": schema.MapAttribute{ + MarkdownDescription: "The many relation of the entity", + Computed: true, + Optional: true, + ElementType: types.ListType{ElemType: types.StringType}, + }, + }, + }, + "created_at": schema.StringAttribute{ + MarkdownDescription: "The creation date of the entity", + Computed: true, + }, + "created_by": schema.StringAttribute{ + MarkdownDescription: "The creator of the entity", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + MarkdownDescription: "The last update date of the entity", + Computed: true, + }, + "updated_by": schema.StringAttribute{ + MarkdownDescription: "The last updater of the entity", + Computed: true, + }, + } +} + +func Schema() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "query": schema.StringAttribute{ + MarkdownDescription: "The search query", + Required: true, + }, + "exclude_calculated_properties": schema.BoolAttribute{ + MarkdownDescription: "Exclude calculated properties", + Optional: true, + }, + "include": schema.ListAttribute{ + MarkdownDescription: "Properties to include in the results", + Optional: true, + ElementType: types.StringType, + }, + "exclude": schema.ListAttribute{ + MarkdownDescription: "Properties to exclude from the results", + Optional: true, + ElementType: types.StringType, + }, + "attach_title_to_relation": schema.BoolAttribute{ + MarkdownDescription: "Attach title to relation", + Optional: true, + }, + "matching_blueprints": schema.ListAttribute{ + MarkdownDescription: "The matching blueprints for the search query", + Computed: true, + ElementType: types.StringType, + }, + "entities": schema.ListNestedAttribute{ + MarkdownDescription: "A list of entities matching the search query", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: EntitySchema(), + }, + }, + } +} + +func (d *SearchDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: SearchDataSourceMarkdownDescription, + Attributes: Schema(), + } +} + +var SearchDataSourceMarkdownDescription = ` + +# Search Data Source + +The search data source allows you to search for entities in Port. + +See the [Port documentation](https://docs.getport.io/search-and-query/) for more information about the search capabilities in Port. + +## Example Usage + +### Search for all entities in a specific blueprint: + +` + "```hcl" + ` + +data "port_search" "all_service" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "Service" }, + ] + }) +} + +` + "\n```" + ` + +### Search for entity with specific identifier in a specific blueprint to create another resource based on the values of the entity: + + +` + "```hcl" + ` + +data "port_search" "ads_service" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "Service" }, + { "operator" : "=", "property" : "$identifier", "value" : "Ads" }, + ] + }) +} + +` + "\n```" + ` + +### Scorecards automation example +In this example we are creating a jira task for each service that its Ownership Scorecard hasn't reached Gold level : + +` + "```hcl" + ` + +data "port_search" "all_services" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "microservice" }, + ] + }) +} + +locals { + // Count the number of services that are not owned by a team with a Gold level + microservice_ownership_without_gold_level = length([ + for entity in data.port_search.all_services.entities : entity.scorecards["ownership"].level + if entity.scorecards["ownership"].level != "Gold" + ]) +} + +// create jira issue per service that is not owned by a team with a Gold level +resource "jira_issue" "microservice_ownership_without_gold_level" { + count = local.microservice_ownership_without_gold_level + issue_type = "Task" + + project_key = "PORT" + + summary = "Service ${data.port_search.backend_services.entities[count.index].title} hasn't reached Gold level in Ownership Scorecard" + description = "[Service](https://app.getport.io/${port_blueprint.microservice.identifier}Entity/${data.port_search.backend_services.entities[count.index].identifier}) is not owned by a team with a Gold level, please assign a team with a Gold level to the service" +} + +` + "\n```" + `` diff --git a/port/search/data_test.go b/port/search/data_test.go new file mode 100644 index 00000000..d98ce40d --- /dev/null +++ b/port/search/data_test.go @@ -0,0 +1,737 @@ +package search_test + +import ( + "fmt" + "github.com/port-labs/terraform-provider-port-labs/v2/internal/utils" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/port-labs/terraform-provider-port-labs/v2/internal/acctest" +) + +func TestAccPortEntity(t *testing.T) { + identifier := utils.GenID() + var testAccActionConfigCreate = fmt.Sprintf(` + resource "port_blueprint" "microservice" { + title = "TF Provider Test BP0" + icon = "Terraform" + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier" = { + "title" = "My String Identifier" + } + } + "number_props" = { + "myNumberIdentifier" = { + "title" = "My Number Identifier" + } + } + "boolean_props" = { + "myBooleanIdentifier" = { + "title" = "My Boolean Identifier" + } + } + "object_props" = { + "myObjectIdentifier" = { + "title" = "My Object Identifier" + } + } + "array_props" = { + "myStringArrayIdentifier" = { + "title" = "My String Array Identifier" + "string_items" = {} + } + "myNumberArrayIdentifier" = { + "title" = "My Number Array Identifier" + "number_items" = {} + } + "myBooleanArrayIdentifier" = { + "title" = "My Boolean Array Identifier" + "boolean_items" = {} + } + "myObjectArrayIdentifier" = { + "title" = "My Object Array Identifier" + "object_items" = {} + } + } + } + } + resource "port_entity" "microservice" { + title = "TF Provider Test Entity0" + blueprint = port_blueprint.microservice.identifier + properties = { + "string_props" = { + "myStringIdentifier" = "My String Value" + } + "number_props" = { + "myNumberIdentifier" = 123.456 + } + "boolean_props" = { + "myBooleanIdentifier" = true + } + "object_props" = { + "myObjectIdentifier" = jsonencode({"foo": "bar"}) + } + "array_props" = { + string_items = { + "myStringArrayIdentifier" = ["My Array Value", "My Array Value2"] + } + number_items = { + "myNumberArrayIdentifier" = [123, 456] + } + boolean_items = { + "myBooleanArrayIdentifier" = [true, false] + } + object_items = { + "myObjectArrayIdentifier" = [jsonencode({"foo": "bar"}), jsonencode({"foo": "bar2"})] + } + } + } + } + `, identifier) + + var testSearchActionQuery = fmt.Sprintf(` + data "port_search" "microservice" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "%s" }, + { "operator" : "=", "property" : "$identifier", "value" : port_entity.microservice.identifier } + ] + }) +}`, 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_entity.microservice", "title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("port_entity.microservice", "blueprint", identifier), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.string_props.myStringIdentifier", "My String Value"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.number_props.myNumberIdentifier", "123.456"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.boolean_props.myBooleanIdentifier", "true"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.object_props.myObjectIdentifier", "{\"foo\":\"bar\"}"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.string_items.myStringArrayIdentifier.0", "My Array Value"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.string_items.myStringArrayIdentifier.1", "My Array Value2"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.number_items.myNumberArrayIdentifier.0", "123"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.number_items.myNumberArrayIdentifier.1", "456"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.boolean_items.myBooleanArrayIdentifier.0", "true"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.boolean_items.myBooleanArrayIdentifier.1", "false"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.object_items.myObjectArrayIdentifier.0", "{\"foo\":\"bar\"}"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.object_items.myObjectArrayIdentifier.1", "{\"foo\":\"bar2\"}"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigCreate + testSearchActionQuery, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.blueprint", identifier), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.string_props.myStringIdentifier", "My String Value"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.number_props.myNumberIdentifier", "123.456"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.boolean_props.myBooleanIdentifier", "true"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.object_props.myObjectIdentifier", "{\"foo\":\"bar\"}"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.string_items.myStringArrayIdentifier.0", "My Array Value"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.string_items.myStringArrayIdentifier.1", "My Array Value2"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.number_items.myNumberArrayIdentifier.0", "123"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.number_items.myNumberArrayIdentifier.1", "456"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.boolean_items.myBooleanArrayIdentifier.0", "true"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.boolean_items.myBooleanArrayIdentifier.1", "false"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.object_items.myObjectArrayIdentifier.0", "{\"foo\":\"bar\"}"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.object_items.myObjectArrayIdentifier.1", "{\"foo\":\"bar2\"}"), + ), + }, + }, + }) +} + +func TestAccPortEntityWithNulls(t *testing.T) { + identifier := utils.GenID() + var testAccActionConfigCreate = fmt.Sprintf(` + resource "port_blueprint" "microservice" { + title = "TF Provider Test BP0" + icon = "Terraform" + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier" = { + "title" = "My String Identifier" + } + } + "number_props" = { + "myNumberIdentifier" = { + "title" = "My Number Identifier" + } + } + "boolean_props" = { + "myBooleanIdentifier" = { + "title" = "My Boolean Identifier" + } + } + "object_props" = { + "myObjectIdentifier" = { + "title" = "My Object Identifier" + } + } + "array_props" = { + "myStringArrayIdentifier" = { + "title" = "My String Array Identifier" + "string_items" = {} + } + "myNumberArrayIdentifier" = { + "title" = "My Number Array Identifier" + "number_items" = {} + } + "myBooleanArrayIdentifier" = { + "title" = "My Boolean Array Identifier" + "boolean_items" = {} + } + "myObjectArrayIdentifier" = { + "title" = "My Object Array Identifier" + "object_items" = {} + } + } + } + } + resource "port_entity" "microservice" { + title = "TF Provider Test Entity0" + blueprint = port_blueprint.microservice.identifier + properties = { + "string_props" = { + "myStringIdentifier" = null + } + "number_props" = { + "myNumberIdentifier" = null + } + "boolean_props" = { + "myBooleanIdentifier" = null + } + "object_props" = { + "myObjectIdentifier" = null + } + "array_props" = { + string_items = { + "myStringArrayIdentifier" = null + } + number_items = { + "myNumberArrayIdentifier" = null + } + boolean_items = { + "myBooleanArrayIdentifier" = null + } + object_items = { + "myObjectArrayIdentifier" = null + } + } + } + } + `, identifier) + + var testSearchActionQuery = fmt.Sprintf(` + data "port_search" "microservice" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "%s" }, + { "operator" : "=", "property" : "$identifier", "value" : port_entity.microservice.identifier } + ] + }) + }`, 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_entity.microservice", "title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("port_entity.microservice", "blueprint", identifier), + resource.TestCheckNoResourceAttr("port_entity.microservice", "properties.string_props.myStringIdentifier"), + resource.TestCheckNoResourceAttr("port_entity.microservice", "properties.number_props.myNumberIdentifier"), + resource.TestCheckNoResourceAttr("port_entity.microservice", "properties.boolean_props.myBooleanIdentifier"), + resource.TestCheckNoResourceAttr("port_entity.microservice", "properties.object_props.myObjectIdentifier"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.string_items.myStringArrayIdentifier.#", "0"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.number_items.myNumberArrayIdentifier.#", "0"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.boolean_items.myBooleanArrayIdentifier.#", "0"), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.array_props.object_items.myObjectArrayIdentifier.#", "0"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigCreate + testSearchActionQuery, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.blueprint", identifier), + resource.TestCheckNoResourceAttr("data.port_search.microservice", "entities.0.properties.string_props.myStringIdentifier"), + resource.TestCheckNoResourceAttr("data.port_search.microservice", "entities.0.properties.number_props.myNumberIdentifier"), + resource.TestCheckNoResourceAttr("data.port_search.microservice", "entities.0.properties.boolean_props.myBooleanIdentifier"), + resource.TestCheckNoResourceAttr("data.port_search.microservice", "entities.0.properties.object_props.myObjectIdentifier"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.string_items.myStringArrayIdentifier.#", "0"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.number_items.myNumberArrayIdentifier.#", "0"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.boolean_items.myBooleanArrayIdentifier.#", "0"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.array_props.object_items.myObjectArrayIdentifier.#", "0"), + ), + }, + }, + }) +} + +func TestAccPortEntityWithRelation(t *testing.T) { + identifier := utils.GenID() + identifier2 := utils.GenID() + var testAccActionConfigCreate = fmt.Sprintf(` + resource "port_blueprint" "microservice" { + title = "TF Provider Test BP0" + icon = "Terraform" + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier" = { + "title" = "My String Identifier" + } + } + } + relations = { + "tfRelation" = { + "title" = "Test Relation" + "target" = port_blueprint.microservice2.identifier + } + } + } + resource "port_blueprint" "microservice2" { + title = "TF Provider Test BP1" + icon = "Terraform" + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier2" = { + "title" = "My String Identifier2" + } + } + } + } + + resource "port_entity" "microservice" { + title = "TF Provider Test Entity0" + blueprint = port_blueprint.microservice.identifier + properties = { + "string_props" = { + "myStringIdentifier" = "My String Value" + } + } + relations = { + single_relations = { + "tfRelation" = port_entity.microservice2.identifier + } + } + } + + resource "port_entity" "microservice2" { + title = "TF Provider Test Entity1" + identifier = "tf-entity-2" + blueprint = port_blueprint.microservice2.identifier + properties = { + "string_props" = { + "myStringIdentifier2" = "My String Value2" + } + } + } + `, identifier, identifier2) + + var testSearchActionQuery1 = fmt.Sprintf(` + data "port_search" "microservice" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "%s" }, + { "operator" : "=", "property" : "$identifier", "value" : port_entity.microservice.identifier } + ] + }) + }`, identifier) + + var testSearchActionQuery2 = fmt.Sprintf(` + data "port_search" "microservice2" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "%s" }, + { "operator" : "=", "property" : "$identifier", "value" : port_entity.microservice2.identifier } + ] + }) + }`, identifier2) + + 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_entity.microservice", "title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("port_entity.microservice", "blueprint", identifier), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.string_props.myStringIdentifier", "My String Value"), + resource.TestCheckResourceAttr("port_entity.microservice", "relations.single_relations.tfRelation", "tf-entity-2"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigCreate + testSearchActionQuery1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.blueprint", identifier), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.string_props.myStringIdentifier", "My String Value"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.relations.single_relations.tfRelation", "tf-entity-2"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigCreate + testSearchActionQuery2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.port_search.microservice2", "entities.0.title", "TF Provider Test Entity1"), + resource.TestCheckResourceAttr("data.port_search.microservice2", "entities.0.blueprint", identifier2), + resource.TestCheckResourceAttr("data.port_search.microservice2", "entities.0.properties.string_props.myStringIdentifier2", "My String Value2"), + ), + }, + }, + }) +} + +func TestAccPortEntityWithManyRelation(t *testing.T) { + identifier1 := utils.GenID() + identifier2 := utils.GenID() + var testAccActionConfigCreate = fmt.Sprintf(` + resource "port_blueprint" "microservice" { + title = "TF Provider Test BP0" + icon = "Terraform" + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier" = { + "title" = "My String Identifier" + } + } + } + relations = { + "tfRelation" = { + "title" = "Test Relation" + "target" = port_blueprint.microservice2.identifier + "many" = true + } + } + } + resource "port_blueprint" "microservice2" { + title = "TF Provider Test BP1" + icon = "Terraform" + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier2" = { + "title" = "My String Identifier2" + } + } + } + } + + resource "port_entity" "microservice" { + title = "TF Provider Test Entity0" + blueprint = port_blueprint.microservice.identifier + properties = { + "string_props" = { + "myStringIdentifier" = "My String Value" + } + } + relations = { + "many_relations" = { + "tfRelation" = [port_entity.microservice2.identifier, port_entity.microservice3.identifier] + } + } + } + + resource "port_entity" "microservice2" { + title = "TF Provider Test Entity1" + identifier = "tf-entity-2" + blueprint = port_blueprint.microservice2.identifier + properties = { + "string_props" = { + "myStringIdentifier2" = "My String Value2" + } + } + } + + resource "port_entity" "microservice3" { + depends_on = [port_entity.microservice2] + title = "TF Provider Test Entity2" + identifier = "tf-entity-3" + blueprint = port_blueprint.microservice2.identifier + properties = { + "string_props" = { + "myStringIdentifier2" = "My String Value3" + } + } + } + `, identifier1, identifier2) + + var testSearchActionQuery1 = fmt.Sprintf(` + data "port_search" "microservice" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "%s" }, + { "operator" : "=", "property" : "$identifier", "value" : port_entity.microservice.identifier } + ] + }) + }`, identifier1) + + var testSearchActionQuery2 = fmt.Sprintf(` + data "port_search" "microservice2" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "%s" }, + { "operator" : "=", "property" : "$identifier", "value" : port_entity.microservice2.identifier } + ] + }) + }`, identifier2) + + var testSearchActionQuery3 = fmt.Sprintf(` + data "port_search" "microservice3" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "%s" }, + { "operator" : "=", "property" : "$identifier", "value" : port_entity.microservice3.identifier } + ] + }) + }`, identifier2) + + 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_entity.microservice", "title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("port_entity.microservice", "blueprint", identifier1), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.string_props.myStringIdentifier", "My String Value"), + resource.TestCheckResourceAttr("port_entity.microservice", "relations.many_relations.tfRelation.0", "tf-entity-2"), + resource.TestCheckResourceAttr("port_entity.microservice", "relations.many_relations.tfRelation.1", "tf-entity-3"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigCreate + testSearchActionQuery1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.blueprint", identifier1), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.string_props.myStringIdentifier", "My String Value"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.relations.many_relations.tfRelation.0", "tf-entity-2"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.relations.many_relations.tfRelation.1", "tf-entity-3"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigCreate + testSearchActionQuery2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.port_search.microservice2", "entities.0.title", "TF Provider Test Entity1"), + resource.TestCheckResourceAttr("data.port_search.microservice2", "entities.0.blueprint", identifier2), + resource.TestCheckResourceAttr("data.port_search.microservice2", "entities.0.properties.string_props.myStringIdentifier2", "My String Value2"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigCreate + testSearchActionQuery3, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.port_search.microservice3", "entities.0.title", "TF Provider Test Entity2"), + resource.TestCheckResourceAttr("data.port_search.microservice3", "entities.0.blueprint", identifier2), + resource.TestCheckResourceAttr("data.port_search.microservice3", "entities.0.properties.string_props.myStringIdentifier2", "My String Value3"), + ), + }, + }, + }) +} + +func TestAccPortEntityWithEmptyRelation(t *testing.T) { + identifier := utils.GenID() + identifier2 := utils.GenID() + var testAccActionConfigCreate = fmt.Sprintf(` + resource "port_blueprint" "microservice" { + title = "TF Provider Test BP0" + icon = "Terraform" + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier" = { + "title" = "My String Identifier" + } + } + } + relations = { + "tfRelation" = { + "title" = "Test Relation" + "target" = port_blueprint.microservice2.identifier + } + } + } + resource "port_blueprint" "microservice2" { + title = "TF Provider Test BP1" + icon = "Terraform" + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier2" = { + "title" = "My String Identifier2" + } + } + } + } + + resource "port_entity" "microservice" { + title = "TF Provider Test Entity0" + blueprint = port_blueprint.microservice.identifier + properties = { + "string_props" = { + "myStringIdentifier" = "My String Value" + } + } + relations = { + single_relations = { + "tfRelation" = null + } + } + } + `, identifier, identifier2) + + var testSearchActionQuery = fmt.Sprintf(` + data "port_search" "microservice" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "%s" }, + { "operator" : "=", "property" : "$identifier", "value" : port_entity.microservice.identifier } + ] + }) + }`, 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_entity.microservice", "title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("port_entity.microservice", "blueprint", identifier), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.string_props.myStringIdentifier", "My String Value"), + resource.TestCheckNoResourceAttr("port_entity.microservice", "relations.single_relations.tfRelation"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigCreate + testSearchActionQuery, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.blueprint", identifier), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.string_props.myStringIdentifier", "My String Value"), + resource.TestCheckNoResourceAttr("data.port_search.microservice", "entities.0.relations.single_relations.tfRelation"), + ), + }, + }, + }) +} + +func TestAccPortEntityUpdateProp(t *testing.T) { + identifier := utils.GenID() + entityIdentifier := utils.GenID() + var testAccActionConfigCreate = fmt.Sprintf(` + resource "port_blueprint" "microservice" { + title = "TF Provider Test BP0" + icon = "Terraform" + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier" = { + "title" = "My String Identifier" + } + } + } + } + resource "port_entity" "microservice" { + title = "TF Provider Test Entity0" + blueprint = port_blueprint.microservice.identifier + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier" = "My String Value" + } + } + }`, identifier, entityIdentifier) + + var testSearchActionQuery = fmt.Sprintf(` + data "port_search" "microservice" { + query = jsonencode({ + "combinator" : "and", "rules" : [ + { "operator" : "=", "property" : "$blueprint", "value" : "%s" }, + { "operator" : "=", "property" : "$identifier", "value" : port_entity.microservice.identifier } + ] + }) + }`, identifier) + + var testAccActionConfigUpdate = fmt.Sprintf(` + resource "port_blueprint" "microservice" { + title = "TF Provider Test BP0" + icon = "Terraform" + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier" = { + "title" = "My String Identifier" + } + } + } + } + resource "port_entity" "microservice" { + title = "TF Provider Test Entity0" + blueprint = port_blueprint.microservice.identifier + identifier = "%s" + properties = { + "string_props" = { + "myStringIdentifier" = "My String Value2" + } + } + }`, identifier, entityIdentifier) + + 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_entity.microservice", "title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("port_entity.microservice", "blueprint", identifier), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.string_props.myStringIdentifier", "My String Value"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigCreate + testSearchActionQuery, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.blueprint", identifier), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.string_props.myStringIdentifier", "My String Value"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigUpdate, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("port_entity.microservice", "title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("port_entity.microservice", "blueprint", identifier), + resource.TestCheckResourceAttr("port_entity.microservice", "properties.string_props.myStringIdentifier", "My String Value2"), + ), + }, + { + Config: acctest.ProviderConfig + testAccActionConfigUpdate + testSearchActionQuery, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.title", "TF Provider Test Entity0"), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.blueprint", identifier), + resource.TestCheckResourceAttr("data.port_search.microservice", "entities.0.properties.string_props.myStringIdentifier", "My String Value2"), + ), + }, + }, + }) +} diff --git a/port/search/model.go b/port/search/model.go new file mode 100644 index 00000000..f1a5a70e --- /dev/null +++ b/port/search/model.go @@ -0,0 +1,89 @@ +package search + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/types" + "strings" +) + +type ArrayPropsModel struct { + StringItems types.Map `tfsdk:"string_items"` + NumberItems types.Map `tfsdk:"number_items"` + BooleanItems types.Map `tfsdk:"boolean_items"` + ObjectItems types.Map `tfsdk:"object_items"` +} + +type EntityPropertiesModel struct { + StringProps map[string]types.String `tfsdk:"string_props"` + NumberProps map[string]types.Float64 `tfsdk:"number_props"` + BooleanProps map[string]types.Bool `tfsdk:"boolean_props"` + ObjectProps map[string]types.String `tfsdk:"object_props"` + ArrayProps *ArrayPropsModel `tfsdk:"array_props"` +} + +type RelationModel struct { + SingleRelation map[string]*string `tfsdk:"single_relations"` + ManyRelations map[string][]string `tfsdk:"many_relations"` +} + +type ScorecardRulesModel struct { + Identifier types.String `tfsdk:"identifier"` + Status types.String `tfsdk:"status"` + Level types.String `tfsdk:"level"` +} + +type ScorecardModel struct { + Rules []ScorecardRulesModel `tfsdk:"rules"` + Level types.String `tfsdk:"level"` +} + +type EntityModel struct { + Identifier types.String `tfsdk:"identifier"` + Blueprint types.String `tfsdk:"blueprint"` + Title types.String `tfsdk:"title"` + Icon types.String `tfsdk:"icon"` + RunID types.String `tfsdk:"run_id"` + CreatedAt types.String `tfsdk:"created_at"` + CreatedBy types.String `tfsdk:"created_by"` + UpdatedAt types.String `tfsdk:"updated_at"` + UpdatedBy types.String `tfsdk:"updated_by"` + Properties *EntityPropertiesModel `tfsdk:"properties"` + Teams []types.String `tfsdk:"teams"` + Scorecards *map[string]ScorecardModel `tfsdk:"scorecards"` + Relations *RelationModel `tfsdk:"relations"` +} + +type SearchDataModel struct { + ID types.String `tfsdk:"id"` + Query types.String `tfsdk:"query"` + ExcludeCalculatedProperties types.Bool `tfsdk:"exclude_calculated_properties"` + Include []types.String `tfsdk:"include"` + Exclude []types.String `tfsdk:"exclude"` + AttachTitleToRelation types.Bool `tfsdk:"attach_title_to_relation"` + MatchingBlueprints []types.String `tfsdk:"matching_blueprints"` + Entities []EntityModel `tfsdk:"entities"` +} + +func (m *SearchDataModel) GenerateID() string { + // Concatenate the model fields into a single string + var sb strings.Builder + sb.WriteString(m.Query.ValueString()) + sb.WriteString(fmt.Sprintf("%t", m.ExcludeCalculatedProperties.ValueBool())) + for _, include := range m.Include { + sb.WriteString(include.ValueString()) + } + for _, exclude := range m.Exclude { + sb.WriteString(exclude.ValueString()) + } + sb.WriteString(fmt.Sprintf("%t", m.AttachTitleToRelation.ValueBool())) + + // Compute the SHA-256 hash of the concatenated string + hash := sha256.Sum256([]byte(sb.String())) + + // Convert the hash to a hexadecimal string + hashString := hex.EncodeToString(hash[:]) + + return hashString +} diff --git a/port/search/searchResultToState.go b/port/search/searchResultToState.go new file mode 100644 index 00000000..404f6c2d --- /dev/null +++ b/port/search/searchResultToState.go @@ -0,0 +1,231 @@ +package search + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/port-labs/terraform-provider-port-labs/v2/internal/cli" +) + +func refreshArrayEntityState(ctx context.Context, state *EntityModel, arrayProperties map[string][]interface{}, blueprint *cli.Blueprint) { + mapStringItems := make(map[string][]*string) + mapNumberItems := make(map[string][]*float64) + mapBooleanItems := make(map[string][]*bool) + mapObjectItems := make(map[string][]*string) + + if state.Properties.ArrayProps == nil { + state.Properties.ArrayProps = &ArrayPropsModel{ + StringItems: types.MapNull(types.ListType{ElemType: types.StringType}), + NumberItems: types.MapNull(types.ListType{ElemType: types.Float64Type}), + BooleanItems: types.MapNull(types.ListType{ElemType: types.BoolType}), + ObjectItems: types.MapNull(types.ListType{ElemType: types.StringType}), + } + } + for k, t := range arrayProperties { + + switch blueprint.Schema.Properties[k].Items["type"] { + case "string": + if t != nil { + for _, item := range t { + stringItem := item.(string) + mapStringItems[k] = append(mapStringItems[k], &stringItem) + } + if len(t) == 0 { + mapStringItems[k] = []*string{} + } + } else { + mapStringItems[k] = nil + } + state.Properties.ArrayProps.StringItems, _ = types.MapValueFrom(ctx, types.ListType{ElemType: types.StringType}, mapStringItems) + case "number": + if t != nil { + for _, item := range t { + floatItem := item.(float64) + mapNumberItems[k] = append(mapNumberItems[k], &floatItem) + } + if len(t) == 0 { + mapNumberItems[k] = []*float64{} + } + } else { + mapNumberItems[k] = nil + } + state.Properties.ArrayProps.NumberItems, _ = types.MapValueFrom(ctx, types.ListType{ElemType: types.Float64Type}, mapNumberItems) + + case "boolean": + if t != nil { + for _, item := range t { + boolItem := item.(bool) + mapBooleanItems[k] = append(mapBooleanItems[k], &boolItem) + } + if len(t) == 0 { + mapBooleanItems[k] = []*bool{} + } + } else { + mapBooleanItems[k] = nil + } + state.Properties.ArrayProps.BooleanItems, _ = types.MapValueFrom(ctx, types.ListType{ElemType: types.BoolType}, mapBooleanItems) + + case "object": + if t != nil { + for _, item := range t { + js, _ := json.Marshal(&item) + stringJs := string(js) + mapObjectItems[k] = append(mapObjectItems[k], &stringJs) + } + if len(t) == 0 { + mapObjectItems[k] = []*string{} + } + } else { + mapObjectItems[k] = nil + } + state.Properties.ArrayProps.ObjectItems, _ = types.MapValueFrom(ctx, types.ListType{ElemType: types.StringType}, mapObjectItems) + + } + } +} + +func refreshPropertiesEntityState(ctx context.Context, state *EntityModel, e *cli.Entity, blueprint *cli.Blueprint) { + state.Properties = &EntityPropertiesModel{} + arrayProperties := make(map[string][]interface{}) + for k, v := range e.Properties { + switch t := v.(type) { + case float64: + if state.Properties.NumberProps == nil { + state.Properties.NumberProps = make(map[string]types.Float64) + } + state.Properties.NumberProps[k] = types.Float64Value(t) + case string: + if state.Properties.StringProps == nil { + state.Properties.StringProps = make(map[string]types.String) + } + state.Properties.StringProps[k] = types.StringValue(t) + case bool: + if state.Properties.BooleanProps == nil { + state.Properties.BooleanProps = make(map[string]types.Bool) + } + state.Properties.BooleanProps[k] = types.BoolValue(t) + case []interface{}: + arrayProperties[k] = t + case interface{}: + if state.Properties.ObjectProps == nil { + state.Properties.ObjectProps = make(map[string]types.String) + } + js, _ := json.Marshal(&t) + state.Properties.ObjectProps[k] = types.StringValue(string(js)) + case nil: + switch blueprint.Schema.Properties[k].Type { + case "string": + if state.Properties.StringProps == nil { + state.Properties.StringProps = make(map[string]types.String) + } + state.Properties.StringProps[k] = types.StringNull() + case "number": + if state.Properties.NumberProps == nil { + state.Properties.NumberProps = make(map[string]types.Float64) + } + state.Properties.NumberProps[k] = types.Float64Null() + case "boolean": + if state.Properties.BooleanProps == nil { + state.Properties.BooleanProps = make(map[string]types.Bool) + } + state.Properties.BooleanProps[k] = types.BoolNull() + case "object": + if state.Properties.ObjectProps == nil { + state.Properties.ObjectProps = make(map[string]types.String) + } + state.Properties.ObjectProps[k] = types.StringNull() + case "array": + arrayProperties[k] = []interface{}(nil) + } + } + } + if len(arrayProperties) != 0 { + refreshArrayEntityState(ctx, state, arrayProperties, blueprint) + } +} + +func refreshRelationsEntityState(state *EntityModel, e *cli.Entity) { + relations := &RelationModel{ + SingleRelation: make(map[string]*string), + ManyRelations: make(map[string][]string), + } + + for identifier, r := range e.Relations { + switch v := r.(type) { + case []interface{}: + if len(v) != 0 { + switch v[0].(type) { + case string: + relations.ManyRelations[identifier] = make([]string, len(v)) + for i, s := range v { + relations.ManyRelations[identifier][i] = s.(string) + } + } + } + + case interface{}: + if v != nil { + value := fmt.Sprintf("%v", v) + relations.SingleRelation[identifier] = &value + } + } + } + + state.Relations = relations +} + +func refreshScorecardsEntityState(state *EntityModel, e *cli.Entity) { + if len(e.Scorecards) != 0 { + state.Scorecards = &map[string]ScorecardModel{} + *state.Scorecards = make(map[string]ScorecardModel) + + for k, v := range e.Scorecards { + rules := make([]ScorecardRulesModel, len(v.Rules)) + for i, r := range v.Rules { + rules[i] = ScorecardRulesModel{ + Identifier: types.StringValue(r.Identifier), + Status: types.StringValue(r.Status), + Level: types.StringValue(r.Level), + } + } + (*state.Scorecards)[k] = ScorecardModel{ + Rules: rules, + Level: types.StringValue(v.Level), + } + } + } +} + +func refreshEntityState(ctx context.Context, e *cli.Entity, b *cli.Blueprint) *EntityModel { + state := &EntityModel{} + state.Identifier = types.StringValue(e.Identifier) + state.Blueprint = types.StringValue(e.Blueprint) + state.Title = types.StringValue(e.Title) + state.CreatedAt = types.StringValue(e.CreatedAt.String()) + state.CreatedBy = types.StringValue(e.CreatedBy) + state.UpdatedAt = types.StringValue(e.UpdatedAt.String()) + state.UpdatedBy = types.StringValue(e.UpdatedBy) + + if len(e.Team) != 0 { + state.Teams = make([]types.String, len(e.Team)) + for i, t := range e.Team { + state.Teams[i] = types.StringValue(t) + } + } + + if len(e.Properties) != 0 { + refreshPropertiesEntityState(ctx, state, e, b) + } + + if len(e.Relations) != 0 { + refreshRelationsEntityState(state, e) + } + + if len(e.Scorecards) != 0 { + refreshScorecardsEntityState(state, e) + } + + return state +} diff --git a/port/search/searchToPortBody.go b/port/search/searchToPortBody.go new file mode 100644 index 00000000..323695d7 --- /dev/null +++ b/port/search/searchToPortBody.go @@ -0,0 +1,22 @@ +package search + +import ( + "github.com/port-labs/terraform-provider-port-labs/v2/internal/cli" + "github.com/port-labs/terraform-provider-port-labs/v2/internal/flex" + "github.com/port-labs/terraform-provider-port-labs/v2/internal/utils" +) + +func searchResourceToPortBody(state *SearchDataModel) (*cli.SearchRequestQuery, error) { + query, err := utils.TerraformJsonStringToGoObject(state.Query.ValueStringPointer()) + if err != nil { + return nil, err + } + + return &cli.SearchRequestQuery{ + Query: query, + ExcludeCalculatedProperties: state.ExcludeCalculatedProperties.ValueBoolPointer(), + Include: flex.TerraformStringListToGoArray(state.Include), + Exclude: flex.TerraformStringListToGoArray(state.Exclude), + AttachTitleToRelation: state.AttachTitleToRelation.ValueBoolPointer(), + }, nil +} diff --git a/provider/provider.go b/provider/provider.go index 3d60e258..11c63b03 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -18,6 +18,7 @@ import ( "github.com/port-labs/terraform-provider-port-labs/v2/port/page" "github.com/port-labs/terraform-provider-port-labs/v2/port/page-permissions" "github.com/port-labs/terraform-provider-port-labs/v2/port/scorecard" + "github.com/port-labs/terraform-provider-port-labs/v2/port/search" "github.com/port-labs/terraform-provider-port-labs/v2/port/team" "github.com/port-labs/terraform-provider-port-labs/v2/port/webhook" "github.com/port-labs/terraform-provider-port-labs/v2/version" @@ -124,6 +125,7 @@ func (p *PortLabsProvider) Configure(ctx context.Context, req provider.Configure } resp.ResourceData = c + resp.DataSourceData = c } @@ -145,5 +147,7 @@ func (p *PortLabsProvider) Resources(ctx context.Context) []func() resource.Reso } func (p *PortLabsProvider) DataSources(ctx context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{} + return []func() datasource.DataSource{ + search.NewSearchDataSource, + } }