diff --git a/examples/data-sources/biganimal_csp_tag/data-source.tf b/examples/data-sources/biganimal_csp_tag/data-source.tf new file mode 100644 index 00000000..31de93fd --- /dev/null +++ b/examples/data-sources/biganimal_csp_tag/data-source.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + biganimal = { + source = "EnterpriseDB/biganimal" + version = "1.0.0" + } + } +} + +data "biganimal_csp_tag" "this" { + project_id = "" # ex: "prj_12345" + cloud_provider_id = "" # ex: "aws" +} + +output "csp_tags" { + value = data.biganimal_csp_tag.this.csp_tags +} diff --git a/examples/resources/biganimal_csp_tag/import.sh b/examples/resources/biganimal_csp_tag/import.sh new file mode 100644 index 00000000..798bac1b --- /dev/null +++ b/examples/resources/biganimal_csp_tag/import.sh @@ -0,0 +1,3 @@ +# terraform import biganimal_cluster. / +# example cloud provider id values bah:aws, bah:azure, bah:gcp, aws, azure, gcp +terraform import biganimal_csp_tag.this prj_deadbeef01234567/aws diff --git a/examples/resources/biganimal_csp_tag/resource.tf b/examples/resources/biganimal_csp_tag/resource.tf new file mode 100644 index 00000000..d641f0a4 --- /dev/null +++ b/examples/resources/biganimal_csp_tag/resource.tf @@ -0,0 +1,48 @@ +terraform { + required_providers { + biganimal = { + source = "EnterpriseDB/biganimal" + version = "1.0.0" + } + random = { + source = "hashicorp/random" + version = "3.6.0" + } + } +} + +resource "biganimal_csp_tag" "this" { + project_id = "" # ex: "prj_12345" + cloud_provider_id = "" # ex cloud-provider-id values ["bah:aws", "bah:azure", "bah:gcp", "aws", "azure", "gcp"] + + add_tags = [ + #{ + # csp_tag_key = "" # ex: "key" + # csp_tag_value = "" # ex: "value" + #}, + #{ + # csp_tag_key = "" + # csp_tag_value = "" + #}, + ] + + delete_tags = [ + #"", # ex: "id" + #"", + ] + + edit_tags = [ + #{ + # csp_tag_id = "" # ex: "id" + # csp_tag_key = "" # ex: "key" + # csp_tag_value = "" # ex: "value" + # status = "OK" + #}, + #{ + # csp_tag_id = "" + # csp_tag_key = "" + # csp_tag_value = "" + # status = "OK" + #}, + ] +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 872256c2..8802e140 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -62,6 +62,11 @@ func (api *API) TagClient() *TagClient { return c } +func (api *API) CSPTagClient() *CSPTagClient { + c := NewCSPTagClient(*api) + return c +} + func BuildAPI(meta any) *API { api, ok := meta.(*API) if !ok { diff --git a/pkg/api/tag_csp_client.go b/pkg/api/tag_csp_client.go new file mode 100644 index 00000000..0369db1c --- /dev/null +++ b/pkg/api/tag_csp_client.go @@ -0,0 +1,67 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/models/common/api" +) + +type CSPTagClient struct { + API +} + +func NewCSPTagClient(api API) *CSPTagClient { + httpClient := http.Client{ + Timeout: 60 * time.Second, + } + + api.HTTPClient = httpClient + tc := CSPTagClient{API: api} + return &tc +} + +func (c CSPTagClient) Put(ctx context.Context, projectID, cloudProviderID string, cspTagReq api.CSPTagRequest) (bool, error) { + b, err := json.Marshal(cspTagReq) + if err != nil { + return false, err + } + + url := fmt.Sprintf("projects/%s/cloud-providers/%s/tags", projectID, cloudProviderID) + _, err = c.doRequest(ctx, http.MethodPut, url, bytes.NewBuffer(b)) + if err != nil { + return false, err + } + + return true, nil +} + +func (c CSPTagClient) Get(ctx context.Context, projectID, cloudProviderID string) (*api.CSPTagResponse, error) { + response := &api.CSPTagResponse{} + + url := fmt.Sprintf("projects/%s/cloud-providers/%s/tags", projectID, cloudProviderID) + + body, err := c.doRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(body, &response) + + return response, err +} + +func (tc CSPTagClient) Delete(ctx context.Context, tagId string) error { + url := fmt.Sprintf("tags/%s", tagId) + + _, err := tc.doRequest(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/models/common/api/csp_tag_request.go b/pkg/models/common/api/csp_tag_request.go new file mode 100644 index 00000000..9b1ecac8 --- /dev/null +++ b/pkg/models/common/api/csp_tag_request.go @@ -0,0 +1,19 @@ +package api + +type CSPTagRequest struct { + AddTags []AddTag `json:"addTags"` + DeleteTags []string `json:"deleteTags"` + EditTags []EditTag `json:"editTags"` +} + +type AddTag struct { + CspTagKey string `json:"cspTagKey"` + CspTagValue string `json:"cspTagValue"` +} + +type EditTag struct { + CSPTagID string `json:"cspTagId"` + CSPTagKey string `json:"cspTagKey"` + CSPTagValue string `json:"cspTagValue"` + Status string `json:"status"` +} diff --git a/pkg/models/common/api/csp_tag_response.go b/pkg/models/common/api/csp_tag_response.go new file mode 100644 index 00000000..c0ec2a2c --- /dev/null +++ b/pkg/models/common/api/csp_tag_response.go @@ -0,0 +1,10 @@ +package api + +type CSPTagResponse struct { + Data []struct { + CSPTagID string `json:"cspTagId"` + CSPTagKey string `json:"cspTagKey"` + CSPTagValue string `json:"cspTagValue"` + Status string `json:"status"` + } `json:"data"` +} diff --git a/pkg/provider/data_source_cluster.go b/pkg/provider/data_source_cluster.go index 9834ba78..602cd4a1 100644 --- a/pkg/provider/data_source_cluster.go +++ b/pkg/provider/data_source_cluster.go @@ -18,34 +18,10 @@ var ( _ datasource.DataSourceWithConfigure = &clusterDataSource{} ) -type PgConfigDatasourceModel struct { - Value types.String `tfsdk:"value"` - Name types.String `tfsdk:"name"` -} - -type StorageDatasourceModel struct { - Throughput types.String `tfsdk:"throughput"` - VolumeProperties types.String `tfsdk:"volume_properties"` - VolumeType types.String `tfsdk:"volume_type"` - Iops types.String `tfsdk:"iops"` - Size types.String `tfsdk:"size"` -} - -type ClusterArchitectureDatasourceModel struct { - Nodes types.Int64 `tfsdk:"nodes"` - Id types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` -} - type clusterDatasourceModel struct { ClusterResourceModel } -type AllowedIpRangesDatasourceModel struct { - CidrBlock types.String `tfsdk:"cidr_block"` - Description types.String `tfsdk:"description"` -} - type clusterDataSource struct { client *api.ClusterClient } diff --git a/pkg/provider/data_source_csp_tag.go b/pkg/provider/data_source_csp_tag.go new file mode 100644 index 00000000..951172d6 --- /dev/null +++ b/pkg/provider/data_source_csp_tag.go @@ -0,0 +1,138 @@ +package provider + +import ( + "context" + + "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/api" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ datasource.DataSource = &cSPTagDataSource{} + _ datasource.DataSourceWithConfigure = &cSPTagDataSource{} +) + +type cSPTagDatasourceModel struct { + CSPTagResourceModel +} + +type cSPTagDataSource struct { + client *api.CSPTagClient +} + +func (c *cSPTagDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_csp_tag" +} + +// Configure adds the provider configured client to the data source. +func (c *cSPTagDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + c.client = req.ProviderData.(*api.API).CSPTagClient() +} + +func (c *cSPTagDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "CSP Tags will enable users to categorize and organize resources across types and improve the efficiency of resource retrieval", + // using Blocks for backward compatible + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, + timeouts.Opts{Create: true, Delete: true, Update: true}, + ), + }, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "project_id": schema.StringAttribute{ + Required: true, + }, + "cloud_provider_id": schema.StringAttribute{ + Required: true, + }, + "add_tags": schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "csp_tag_key": schema.StringAttribute{ + Computed: true, + }, + "csp_tag_value": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + "delete_tags": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + }, + "edit_tags": schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "csp_tag_id": schema.StringAttribute{ + Computed: true, + }, + "csp_tag_key": schema.StringAttribute{ + Computed: true, + }, + "csp_tag_value": schema.StringAttribute{ + Computed: true, + }, + "status": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + "csp_tags": schema.ListNestedAttribute{ + Description: "CSP Tags on cluster", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "csp_tag_id": schema.StringAttribute{ + Computed: true, + }, + "csp_tag_key": schema.StringAttribute{ + Computed: true, + }, + "csp_tag_value": schema.StringAttribute{ + Computed: true, + }, + "status": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + }, + } +} + +func (c *cSPTagDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data cSPTagDatasourceModel + diags := req.Config.Get(ctx, &data.CSPTagResourceModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if err := readCSPTag(ctx, c.client, &data.CSPTagResourceModel); err != nil { + if !appendDiagFromBAErr(err, &resp.Diagnostics) { + resp.Diagnostics.AddError("Error reading cluster", err.Error()) + } + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, data.CSPTagResourceModel)...) +} + +func NewCSPTagDataSource() datasource.DataSource { + return &cSPTagDataSource{} +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index c83891c4..a7ce26ec 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -193,6 +193,7 @@ func (b bigAnimalProvider) DataSources(ctx context.Context) []func() datasource. NewRegionsDataSource, NewAnalyticsClusterDataSource, NewTagDataSource, + NewCSPTagDataSource, } } @@ -205,5 +206,6 @@ func (b bigAnimalProvider) Resources(ctx context.Context) []func() resource.Reso NewFAReplicaResource, NewAnalyticsClusterResource, NewTagResource, + NewCSPTagResource, } } diff --git a/pkg/provider/resource_csp_tag.go b/pkg/provider/resource_csp_tag.go new file mode 100644 index 00000000..4c2f8c40 --- /dev/null +++ b/pkg/provider/resource_csp_tag.go @@ -0,0 +1,319 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/api" + commonApi "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/models/common/api" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +var ( + _ resource.Resource = &cSPTagResource{} + _ resource.ResourceWithConfigure = &cSPTagResource{} +) + +type CSPTagResourceModel struct { + ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` + CloudProviderID types.String `tfsdk:"cloud_provider_id"` + AddTags []addTag `tfsdk:"add_tags"` + DeleteTags types.List `tfsdk:"delete_tags"` + EditTags []CSPTag `tfsdk:"edit_tags"` + CSPTags types.List `tfsdk:"csp_tags"` + + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type addTag struct { + CspTagKey types.String `tfsdk:"csp_tag_key"` + CspTagValue types.String `tfsdk:"csp_tag_value"` +} + +type CSPTag struct { + CSPTagID types.String `tfsdk:"csp_tag_id"` + CSPTagKey types.String `tfsdk:"csp_tag_key"` + CSPTagValue types.String `tfsdk:"csp_tag_value"` + Status types.String `tfsdk:"status"` +} + +type cSPTagResource struct { + client *api.CSPTagClient +} + +func (tr *cSPTagResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + tr.client = req.ProviderData.(*api.API).CSPTagClient() +} + +func (tr *cSPTagResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_csp_tag" +} + +func (tf *cSPTagResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "CSP Tags will enable users to categorize and organize resources across types and improve the efficiency of resource retrieval", + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, + timeouts.Opts{Create: true, Delete: true, Update: true}), + }, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "project_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "cloud_provider_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "add_tags": schema.ListNestedAttribute{ + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "csp_tag_key": schema.StringAttribute{ + Required: true, + }, + "csp_tag_value": schema.StringAttribute{ + Required: true, + }, + }, + }, + PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, + }, + "delete_tags": schema.ListAttribute{ + Required: true, + PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, + ElementType: types.StringType, + }, + "edit_tags": schema.ListNestedAttribute{ + Description: "", + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "csp_tag_id": schema.StringAttribute{ + Required: true, + }, + "csp_tag_key": schema.StringAttribute{ + Required: true, + }, + "csp_tag_value": schema.StringAttribute{ + Required: true, + }, + "status": schema.StringAttribute{ + Required: true, + }, + }, + }, + PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, + }, + "csp_tags": schema.ListNestedAttribute{ + Description: "", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "csp_tag_id": schema.StringAttribute{ + Computed: true, + }, + "csp_tag_key": schema.StringAttribute{ + Computed: true, + }, + "csp_tag_value": schema.StringAttribute{ + Computed: true, + }, + "status": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + }, + } +} + +func (tr *cSPTagResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config CSPTagResourceModel + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + cSPTagRequest := commonApi.CSPTagRequest{} + cSPTagRequest.AddTags = []commonApi.AddTag{} + cSPTagRequest.DeleteTags = []string{} + cSPTagRequest.EditTags = []commonApi.EditTag{} + + for _, addTag := range config.AddTags { + cSPTagRequest.AddTags = append(cSPTagRequest.AddTags, commonApi.AddTag{ + CspTagKey: addTag.CspTagKey.ValueString(), + CspTagValue: addTag.CspTagValue.ValueString(), + }) + } + + for _, deleteTag := range config.DeleteTags.Elements() { + cSPTagRequest.DeleteTags = append(cSPTagRequest.DeleteTags, deleteTag.(basetypes.StringValue).ValueString()) + } + + for _, editTag := range config.EditTags { + cSPTagRequest.EditTags = append(cSPTagRequest.EditTags, commonApi.EditTag{ + CSPTagID: editTag.CSPTagID.ValueString(), + CSPTagKey: editTag.CSPTagKey.ValueString(), + CSPTagValue: editTag.CSPTagValue.ValueString(), + }) + } + + _, err := tr.client.Put(ctx, config.ProjectID.ValueString(), config.CloudProviderID.ValueString(), cSPTagRequest) + if err != nil { + if !appendDiagFromBAErr(err, &resp.Diagnostics) { + resp.Diagnostics.AddError("Error creating tag", err.Error()) + } + return + } + + err = readCSPTag(ctx, tr.client, &config) + if err != nil { + if !appendDiagFromBAErr(err, &resp.Diagnostics) { + resp.Diagnostics.AddError("Error reading tag", err.Error()) + } + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, config)...) +} + +func (tr *cSPTagResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state CSPTagResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err := readCSPTag(ctx, tr.client, &state) + if err != nil { + if !appendDiagFromBAErr(err, &resp.Diagnostics) { + resp.Diagnostics.AddError("Error reading tag", err.Error()) + } + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func readCSPTag(ctx context.Context, client *api.CSPTagClient, resource *CSPTagResourceModel) error { + cSPTagResp, err := client.Get(ctx, resource.ProjectID.ValueString(), resource.CloudProviderID.ValueString()) + if err != nil { + return err + } + + resource.ID = types.StringValue(fmt.Sprintf("%s/%s", resource.ProjectID.ValueString(), resource.CloudProviderID.ValueString())) + cSPTagsElems := []attr.Value{} + for _, respCSPTag := range cSPTagResp.Data { + cSPTagsElems = append( + cSPTagsElems, types.ObjectValueMust(resource.CSPTags.ElementType(ctx).(types.ObjectType).AttributeTypes(), + map[string]attr.Value{ + "csp_tag_id": types.StringValue(respCSPTag.CSPTagID), + "csp_tag_key": types.StringValue(respCSPTag.CSPTagKey), + "csp_tag_value": types.StringValue(respCSPTag.CSPTagValue), + "status": types.StringValue(respCSPTag.Status), + }, + )) + } + + resource.CSPTags = types.ListValueMust(resource.CSPTags.ElementType(ctx), cSPTagsElems) + return nil +} + +func (tr *cSPTagResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan CSPTagResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + cSPTagRequest := commonApi.CSPTagRequest{} + cSPTagRequest.AddTags = []commonApi.AddTag{} + cSPTagRequest.DeleteTags = []string{} + cSPTagRequest.EditTags = []commonApi.EditTag{} + for _, addTag := range plan.AddTags { + cSPTagRequest.AddTags = append(cSPTagRequest.AddTags, commonApi.AddTag{ + CspTagKey: addTag.CspTagKey.ValueString(), + CspTagValue: addTag.CspTagValue.ValueString(), + }) + } + for _, deleteTag := range plan.DeleteTags.Elements() { + cSPTagRequest.DeleteTags = append(cSPTagRequest.DeleteTags, deleteTag.(basetypes.StringValue).ValueString()) + } + for _, editTag := range plan.EditTags { + cSPTagRequest.EditTags = append(cSPTagRequest.EditTags, commonApi.EditTag{ + CSPTagID: editTag.CSPTagID.ValueString(), + CSPTagKey: editTag.CSPTagKey.ValueString(), + CSPTagValue: editTag.CSPTagValue.ValueString(), + Status: editTag.Status.ValueString(), + }) + } + + _, err := tr.client.Put(ctx, plan.ProjectID.ValueString(), plan.CloudProviderID.ValueString(), cSPTagRequest) + if err != nil { + if !appendDiagFromBAErr(err, &resp.Diagnostics) { + resp.Diagnostics.AddError("Error creating tag", err.Error()) + } + return + } + + err = readCSPTag(ctx, tr.client, &plan) + if err != nil { + if !appendDiagFromBAErr(err, &resp.Diagnostics) { + resp.Diagnostics.AddError("Error reading tag", err.Error()) + } + return + } + + plan.ID = types.StringValue(fmt.Sprintf("%s/%s", plan.ProjectID.ValueString(), plan.CloudProviderID.ValueString())) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (tr *cSPTagResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddWarning( + "Delete operation is not supported for CSP Tag resource", + "Please delete csp tags using an update operation on the field 'delete_tags'", + ) +} + +func (tr *cSPTagResource) 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( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: project_id/cloud_provider_id. Got: %q", req.ID), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cloud_provider_id"), idParts[1])...) +} + +func NewCSPTagResource() resource.Resource { + return &cSPTagResource{} +}