diff --git a/docs/data-sources/resourcemanager_folder.md b/docs/data-sources/resourcemanager_folder.md new file mode 100644 index 000000000..e13cbd08f --- /dev/null +++ b/docs/data-sources/resourcemanager_folder.md @@ -0,0 +1,39 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_resourcemanager_folder Data Source - stackit" +subcategory: "" +description: |- + Resource Manager folder data source schema. To identify the folder, you need to provide the container_id. + ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. +--- + +# stackit_resourcemanager_folder (Data Source) + +Resource Manager folder data source schema. To identify the folder, you need to provide the container_id. + +~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. + +## Example Usage + +```terraform +data "stackit_resourcemanager_folder" "example" { + container_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `container_id` (String) Folder container ID. Globally unique, user-friendly identifier. + +### Read-Only + +- `creation_time` (String) Date-time at which the folder was created. +- `folder_id` (String) Folder UUID identifier. Globally unique folder identifier +- `id` (String) Terraform's internal resource ID. It is structured as "`container_id`". +- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}. +- `name` (String) The name of the folder. +- `parent_container_id` (String) Parent resource identifier. Both container ID (user-friendly) and UUID are supported. +- `update_time` (String) Date-time at which the folder was last modified. diff --git a/docs/data-sources/resourcemanager_project.md b/docs/data-sources/resourcemanager_project.md index d8a358093..6b9ff69c9 100644 --- a/docs/data-sources/resourcemanager_project.md +++ b/docs/data-sources/resourcemanager_project.md @@ -29,7 +29,9 @@ data "stackit_resourcemanager_project" "example" { ### Read-Only +- `creation_time` (String) Date-time at which the project was created. - `id` (String) Terraform's internal data source. ID. It is structured as "`container_id`". - `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64} - `name` (String) Project name. - `parent_container_id` (String) Parent resource identifier. Both container ID (user-friendly) and UUID are supported +- `update_time` (String) Date-time at which the project was last modified. diff --git a/docs/resources/resourcemanager_folder.md b/docs/resources/resourcemanager_folder.md new file mode 100644 index 000000000..aaa80afc7 --- /dev/null +++ b/docs/resources/resourcemanager_folder.md @@ -0,0 +1,64 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_resourcemanager_folder Resource - stackit" +subcategory: "" +description: |- + Resource Manager folder resource schema. + ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. +--- + +# stackit_resourcemanager_folder (Resource) + +Resource Manager folder resource schema. + +~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. + +## Example Usage + +```terraform +resource "stackit_resourcemanager_folder" "example" { + name = "example-folder" + owner_email = "foo.bar@stackit.cloud" + parent_container_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +# Note: +# You can add projects under folders. +# However, when deleting a project, be aware: +# - Projects may remain "invisible" for up to 7 days after deletion +# - During this time, deleting the parent folder may fail because the project is still technically linked +resource "stackit_resourcemanager_project" "example_project" { + name = "example-project" + owner_email = "foo.bar@stackit.cloud" + parent_container_id = stackit_resourcemanager_folder.example.container_id +} + +# Only use the import statement, if you want to import an existing resourcemanager folder +# Note: There will be a conflict which needs to be resolved manually. +# Must set a configuration value for the owner_email attribute as the provider has marked it as required. +import { + to = stackit_resourcemanager_folder.import-example + id = var.container_id +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the folder. +- `owner_email` (String) Email address of the owner of the folder. This value is only considered during creation. Changing it afterwards will have no effect. +- `parent_container_id` (String) Parent resource identifier. Both container ID (user-friendly) and UUID are supported. + +### Optional + +- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}. + +### Read-Only + +- `container_id` (String) Folder container ID. Globally unique, user-friendly identifier. +- `creation_time` (String) Date-time at which the folder was created. +- `folder_id` (String) Folder UUID identifier. Globally unique folder identifier +- `id` (String) Terraform's internal resource ID. It is structured as "`container_id`". +- `update_time` (String) Date-time at which the folder was last modified. diff --git a/docs/resources/resourcemanager_project.md b/docs/resources/resourcemanager_project.md index 9ffa5ebf5..382cc2f78 100644 --- a/docs/resources/resourcemanager_project.md +++ b/docs/resources/resourcemanager_project.md @@ -3,13 +3,13 @@ page_title: "stackit_resourcemanager_project Resource - stackit" subcategory: "" description: |- - Resource Manager project resource schema. To use this resource, it is required that you set the service account email in the provider configuration. + Resource Manager project resource schema. -> In case you're getting started with an empty STACKIT organization and want to use this resource to create projects in it, check out this guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/stackit_org_service_account for how to create a service account which you can use for authentication in the STACKIT Terraform provider. --- # stackit_resourcemanager_project (Resource) -Resource Manager project resource schema. To use this resource, it is required that you set the service account email in the provider configuration. +Resource Manager project resource schema. -> In case you're getting started with an empty STACKIT organization and want to use this resource to create projects in it, check out [this guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/stackit_org_service_account) for how to create a service account which you can use for authentication in the STACKIT Terraform provider. @@ -52,5 +52,7 @@ To create a project within a STACKIT Network Area, setting the label `networkAre ### Read-Only - `container_id` (String) Project container ID. Globally unique, user-friendly identifier. +- `creation_time` (String) Date-time at which the project was created. - `id` (String) Terraform's internal resource ID. It is structured as "`container_id`". - `project_id` (String) Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project. +- `update_time` (String) Date-time at which the project was last modified. diff --git a/examples/data-sources/stackit_resourcemanager_folder/data-source.tf b/examples/data-sources/stackit_resourcemanager_folder/data-source.tf new file mode 100644 index 000000000..a91313e9f --- /dev/null +++ b/examples/data-sources/stackit_resourcemanager_folder/data-source.tf @@ -0,0 +1,3 @@ +data "stackit_resourcemanager_folder" "example" { + container_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} \ No newline at end of file diff --git a/examples/resources/stackit_resourcemanager_folder/resource.tf b/examples/resources/stackit_resourcemanager_folder/resource.tf new file mode 100644 index 000000000..d4d782fac --- /dev/null +++ b/examples/resources/stackit_resourcemanager_folder/resource.tf @@ -0,0 +1,24 @@ +resource "stackit_resourcemanager_folder" "example" { + name = "example-folder" + owner_email = "foo.bar@stackit.cloud" + parent_container_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +# Note: +# You can add projects under folders. +# However, when deleting a project, be aware: +# - Projects may remain "invisible" for up to 7 days after deletion +# - During this time, deleting the parent folder may fail because the project is still technically linked +resource "stackit_resourcemanager_project" "example_project" { + name = "example-project" + owner_email = "foo.bar@stackit.cloud" + parent_container_id = stackit_resourcemanager_folder.example.container_id +} + +# Only use the import statement, if you want to import an existing resourcemanager folder +# Note: There will be a conflict which needs to be resolved manually. +# Must set a configuration value for the owner_email attribute as the provider has marked it as required. +import { + to = stackit_resourcemanager_folder.import-example + id = var.container_id +} \ No newline at end of file diff --git a/stackit/internal/services/resourcemanager/folder/datasource.go b/stackit/internal/services/resourcemanager/folder/datasource.go new file mode 100644 index 000000000..bbcce9240 --- /dev/null +++ b/stackit/internal/services/resourcemanager/folder/datasource.go @@ -0,0 +1,185 @@ +package folder + +import ( + "context" + "fmt" + "net/http" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &folderDataSource{} + _ datasource.DataSourceWithConfigure = &folderDataSource{} +) + +// NewFolderDataSource is a helper function to simplify the provider implementation. +func NewFolderDataSource() datasource.DataSource { + return &folderDataSource{} +} + +// folderDataSource is the data source implementation. +type folderDataSource struct { + client *resourcemanager.APIClient +} + +// Metadata returns the data source type name. +func (d *folderDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_resourcemanager_folder" +} + +func (d *folderDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_resourcemanager_folder", "datasource") + if resp.Diagnostics.HasError() { + return + } + + apiClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "Resource Manager client configured") +} + +// Schema defines the schema for the data source. +func (d *folderDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Resource Manager folder data source schema. To identify the folder, you need to provide the container_id.", + "id": "Terraform's internal resource ID. It is structured as \"`container_id`\".", + "container_id": "Folder container ID. Globally unique, user-friendly identifier.", + "folder_id": "Folder UUID identifier. Globally unique folder identifier", + "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported.", + "name": "The name of the folder.", + "labels": "Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}.", + "owner_email": "Email address of the owner of the folder. This value is only considered during creation. Changing it afterwards will have no effect.", + "creation_time": "Date-time at which the folder was created.", + "update_time": "Date-time at which the folder was last modified.", + } + + resp.Schema = schema.Schema{ + Description: features.AddBetaDescription(descriptions["main"], core.Datasource), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "container_id": schema.StringAttribute{ + Description: descriptions["container_id"], + Validators: []validator.String{ + validate.NoSeparator(), + }, + Required: true, + }, + "folder_id": schema.StringAttribute{ + Description: descriptions["folder_id"], + Computed: true, + Validators: []validator.String{ + validate.UUID(), + }, + }, + "parent_container_id": schema.StringAttribute{ + Description: descriptions["parent_container_id"], + Computed: true, + Validators: []validator.String{ + validate.NoSeparator(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.LengthAtMost(63), + }, + }, + "labels": schema.MapAttribute{ + Description: descriptions["labels"], + ElementType: types.StringType, + Computed: true, + Validators: []validator.Map{ + mapvalidator.KeysAre( + stringvalidator.RegexMatches( + regexp.MustCompile(`[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), + "must match expression"), + ), + mapvalidator.ValueStringsAre( + stringvalidator.RegexMatches( + regexp.MustCompile(`[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), + "must match expression"), + ), + }, + }, + "creation_time": schema.StringAttribute{ + Description: descriptions["creation_time"], + Computed: true, + }, + "update_time": schema.StringAttribute{ + Description: descriptions["update_time"], + Computed: true, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *folderDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + containerId := model.ContainerId.ValueString() + ctx = tflog.SetField(ctx, "container_id", containerId) + + folderResp, err := d.client.GetFolderDetails(ctx, containerId).Execute() + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading folder", + fmt.Sprintf("folder with ID %q does not exist.", containerId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("folder with ID %q not found or forbidden access", containerId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + err = mapFolderFields(ctx, folderResp, &model, &resp.State) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading folder", fmt.Sprintf("Processing API response: %v", err)) + return + } + + diags = resp.State.Set(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Resource Manager folder read") +} diff --git a/stackit/internal/services/resourcemanager/folder/resource.go b/stackit/internal/services/resourcemanager/folder/resource.go new file mode 100644 index 000000000..6154ab478 --- /dev/null +++ b/stackit/internal/services/resourcemanager/folder/resource.go @@ -0,0 +1,510 @@ +package folder + +import ( + "context" + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &folderResource{} + _ resource.ResourceWithConfigure = &folderResource{} + _ resource.ResourceWithImportState = &folderResource{} +) + +const ( + projectOwnerRole = "owner" +) + +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + FolderId types.String `tfsdk:"folder_id"` + ContainerId types.String `tfsdk:"container_id"` + ContainerParentId types.String `tfsdk:"parent_container_id"` + Name types.String `tfsdk:"name"` + Labels types.Map `tfsdk:"labels"` + CreationTime types.String `tfsdk:"creation_time"` + UpdateTime types.String `tfsdk:"update_time"` +} + +type ResourceModel struct { + Model + OwnerEmail types.String `tfsdk:"owner_email"` +} + +// NewFolderResource is a helper function to simplify the provider implementation. +func NewFolderResource() resource.Resource { + return &folderResource{} +} + +// folderResource is the resource implementation. +type folderResource struct { + client *resourcemanager.APIClient +} + +// Metadata returns the resource type name. +func (r *folderResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_resourcemanager_folder" +} + +// Configure adds the provider configured client to the resource. +func (r *folderResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_resourcemanager_folder", "resource") + if resp.Diagnostics.HasError() { + return + } + + apiClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Resource Manager client configured") +} + +// Schema defines the schema for the resource. +func (r *folderResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Resource Manager folder resource schema.", + "id": "Terraform's internal resource ID. It is structured as \"`container_id`\".", + "container_id": "Folder container ID. Globally unique, user-friendly identifier.", + "folder_id": "Folder UUID identifier. Globally unique folder identifier", + "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported.", + "name": "The name of the folder.", + "labels": "Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}.", + "owner_email": "Email address of the owner of the folder. This value is only considered during creation. Changing it afterwards will have no effect.", + "creation_time": "Date-time at which the folder was created.", + "update_time": "Date-time at which the folder was last modified.", + } + + resp.Schema = schema.Schema{ + Description: features.AddBetaDescription(descriptions["main"], core.Resource), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "container_id": schema.StringAttribute{ + Description: descriptions["container_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.NoSeparator(), + }, + }, + "folder_id": schema.StringAttribute{ + Description: descriptions["folder_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.UUID(), + }, + }, + "parent_container_id": schema.StringAttribute{ + Description: descriptions["parent_container_id"], + Required: true, + Validators: []validator.String{ + validate.NoSeparator(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.LengthAtMost(63), + }, + }, + "labels": schema.MapAttribute{ + Description: descriptions["labels"], + ElementType: types.StringType, + Optional: true, + Validators: []validator.Map{ + mapvalidator.KeysAre( + stringvalidator.RegexMatches( + regexp.MustCompile(`[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), + "must match expression"), + ), + mapvalidator.ValueStringsAre( + stringvalidator.RegexMatches( + regexp.MustCompile(`^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), + "must match expression"), + ), + }, + }, + "owner_email": schema.StringAttribute{ + Description: descriptions["owner_email"], + Required: true, + }, + "creation_time": schema.StringAttribute{ + Description: descriptions["creation_time"], + Computed: true, + }, + "update_time": schema.StringAttribute{ + Description: descriptions["update_time"], + Computed: true, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *folderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + tflog.Info(ctx, "creating folder") + var model ResourceModel + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + containerParentId := model.ContainerParentId.ValueString() + folderName := model.Name.ValueString() + ctx = tflog.SetField(ctx, "container_parent_id", containerParentId) + ctx = tflog.SetField(ctx, "folder_name", folderName) + + // Generate API request body from model + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating folder", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + folderCreateResp, err := r.client.CreateFolder(ctx).CreateFolderPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating folder", fmt.Sprintf("Calling API: %v", err)) + return + } + + if folderCreateResp.ContainerId == nil || *folderCreateResp.ContainerId == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating folder", "Container ID is missing") + return + } + + // This sleep is currently needed due to the IAM Cache. + time.Sleep(10 * time.Second) + + folderGetResponse, err := r.client.GetFolderDetails(ctx, *folderCreateResp.ContainerId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating folder", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFolderFields(ctx, folderGetResponse, &model.Model, &resp.State) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "API response processing error", err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + tflog.Info(ctx, "Folder created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *folderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model ResourceModel + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + containerId := model.ContainerId.ValueString() + folderName := model.Name.ValueString() + ctx = tflog.SetField(ctx, "folder_name", folderName) + ctx = tflog.SetField(ctx, "container_id", containerId) + + folderResp, err := r.client.GetFolderDetails(ctx, containerId).Execute() + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if ok && oapiErr.StatusCode == http.StatusForbidden { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading folder", fmt.Sprintf("Calling API: %v", err)) + return + } + + err = mapFolderFields(ctx, folderResp, &model.Model, &resp.State) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading folder", fmt.Sprintf("Processing API response: %v", err)) + return + } + + // Set refreshed model + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Resource Manager folder read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *folderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model ResourceModel + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + containerId := model.ContainerId.ValueString() + ctx = tflog.SetField(ctx, "container_id", containerId) + + // Generate API request body from model + payload, err := toUpdatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating folder", fmt.Sprintf("Creating API payload: %v", err)) + return + } + // Update existing folder + _, err = r.client.PartialUpdateFolder(ctx, containerId).PartialUpdateFolderPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating folder", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Fetch updated folder + folderResp, err := r.client.GetFolderDetails(ctx, containerId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating folder", fmt.Sprintf("Calling API for updated data: %v", err)) + return + } + + err = mapFolderFields(ctx, folderResp, &model.Model, &resp.State) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating folder", fmt.Sprintf("Processing API response: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Resource Manager folder updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *folderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from state + var model ResourceModel + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + containerId := model.ContainerId.ValueString() + ctx = tflog.SetField(ctx, "container_id", containerId) + + // Delete existing folder + err := r.client.DeleteFolder(ctx, containerId).Execute() + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error deleting folder. Deletion may fail because associated projects remain hidden for up to 7 days after user deletion due to technical requirements.", + fmt.Sprintf("Calling API: %v", err), + ) + return + } + + tflog.Info(ctx, "Resource Manager folder deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the resource import identifier is: container_id +func (r *folderResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + if len(idParts) != 1 || idParts[0] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing folder", + fmt.Sprintf("Expected import identifier with format: [container_id] Got: %q", req.ID), + ) + return + } + + ctx = tflog.SetField(ctx, "container_id", req.ID) + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("container_id"), req.ID)...) + tflog.Info(ctx, "Resource Manager folder state imported") +} + +// mapFolderFields maps folder fields from a response into the Terraform model and optionally updates state. +func mapFolderFields( + ctx context.Context, + folderGetResponse *resourcemanager.GetFolderDetailsResponse, + model *Model, + state *tfsdk.State, +) error { + if folderGetResponse == nil { + return fmt.Errorf("folder get response is nil") + } + + var folderId string + if model.FolderId.ValueString() != "" { + folderId = model.FolderId.ValueString() + } else if folderGetResponse.FolderId != nil { + folderId = *folderGetResponse.FolderId + } else { + return fmt.Errorf("folder id not present") + } + + var containerId string + if model.ContainerId.ValueString() != "" { + containerId = model.ContainerId.ValueString() + } else if folderGetResponse.ContainerId != nil { + containerId = *folderGetResponse.ContainerId + } else { + return fmt.Errorf("container id not present") + } + + var err error + var tfLabels basetypes.MapValue + if folderGetResponse.Labels != nil && len(*folderGetResponse.Labels) > 0 { + tfLabels, err = conversion.ToTerraformStringMap(ctx, *folderGetResponse.Labels) + if err != nil { + return fmt.Errorf("converting to StringValue map: %w", err) + } + } else { + tfLabels = types.MapNull(types.StringType) + } + + var containerParentIdTF basetypes.StringValue + if folderGetResponse.Parent != nil { + if _, err := uuid.Parse(model.ContainerParentId.ValueString()); err == nil { + // the provided containerParent is the UUID identifier + containerParentIdTF = types.StringPointerValue(folderGetResponse.Parent.Id) + } else { + // the provided containerParent is the user-friendly container id + containerParentIdTF = types.StringPointerValue(folderGetResponse.Parent.ContainerId) + } + } else { + containerParentIdTF = types.StringNull() + } + + model.Id = types.StringValue(containerId) + model.FolderId = types.StringValue(folderId) + model.ContainerId = types.StringValue(containerId) + model.ContainerParentId = containerParentIdTF + model.Name = types.StringPointerValue(folderGetResponse.Name) + model.Labels = tfLabels + model.CreationTime = types.StringValue(folderGetResponse.CreationTime.Format(time.RFC3339)) + model.UpdateTime = types.StringValue(folderGetResponse.UpdateTime.Format(time.RFC3339)) + + if state != nil { + diags := diag.Diagnostics{} + diags.Append(state.SetAttribute(ctx, path.Root("id"), model.Id)...) + diags.Append(state.SetAttribute(ctx, path.Root("folder_id"), model.FolderId)...) + diags.Append(state.SetAttribute(ctx, path.Root("container_id"), model.ContainerId)...) + diags.Append(state.SetAttribute(ctx, path.Root("parent_container_id"), model.ContainerParentId)...) + diags.Append(state.SetAttribute(ctx, path.Root("name"), model.Name)...) + diags.Append(state.SetAttribute(ctx, path.Root("labels"), model.Labels)...) + diags.Append(state.SetAttribute(ctx, path.Root("creation_time"), model.CreationTime)...) + diags.Append(state.SetAttribute(ctx, path.Root("update_time"), model.UpdateTime)...) + if diags.HasError() { + return fmt.Errorf("update terraform state: %w", core.DiagsToError(diags)) + } + } + + return nil +} + +func toMembersPayload(model *ResourceModel) (*[]resourcemanager.Member, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + if model.OwnerEmail.IsNull() { + return nil, fmt.Errorf("owner_email is null") + } + + return &[]resourcemanager.Member{ + { + Subject: model.OwnerEmail.ValueStringPointer(), + Role: sdkUtils.Ptr(projectOwnerRole), + }, + }, nil +} + +func toCreatePayload(model *ResourceModel) (*resourcemanager.CreateFolderPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + members, err := toMembersPayload(model) + if err != nil { + return nil, fmt.Errorf("processing members: %w", err) + } + + modelLabels := model.Labels.Elements() + labels, err := conversion.ToOptStringMap(modelLabels) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + return &resourcemanager.CreateFolderPayload{ + ContainerParentId: conversion.StringValueToPointer(model.ContainerParentId), + Labels: labels, + Members: members, + Name: conversion.StringValueToPointer(model.Name), + }, nil +} + +func toUpdatePayload(model *ResourceModel) (*resourcemanager.PartialUpdateFolderPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + modelLabels := model.Labels.Elements() + labels, err := conversion.ToOptStringMap(modelLabels) + if err != nil { + return nil, fmt.Errorf("converting to GO map: %w", err) + } + + return &resourcemanager.PartialUpdateFolderPayload{ + ContainerParentId: conversion.StringValueToPointer(model.ContainerParentId), + Name: conversion.StringValueToPointer(model.Name), + Labels: labels, + }, nil +} diff --git a/stackit/internal/services/resourcemanager/folder/resource_test.go b/stackit/internal/services/resourcemanager/folder/resource_test.go new file mode 100644 index 000000000..500f66f19 --- /dev/null +++ b/stackit/internal/services/resourcemanager/folder/resource_test.go @@ -0,0 +1,396 @@ +package folder + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" +) + +func TestMapFolderFields(t *testing.T) { + testUUID := uuid.New().String() + baseTime := time.Now() + createTime := baseTime + updateTime := baseTime.Add(1 * time.Hour) + + tests := []struct { + description string + uuidContainerParentId bool + projectResp *resourcemanager.GetFolderDetailsResponse + expected Model + expectedLabels *map[string]string + isValid bool + }{ + { + description: "default_ok", + uuidContainerParentId: false, + projectResp: &resourcemanager.GetFolderDetailsResponse{ + ContainerId: utils.Ptr("cid"), + FolderId: utils.Ptr("fid"), + CreationTime: &createTime, + UpdateTime: &updateTime, + }, + expected: Model{ + Id: types.StringValue("cid"), + ContainerId: types.StringValue("cid"), + FolderId: types.StringValue("fid"), + ContainerParentId: types.StringNull(), + Name: types.StringNull(), + CreationTime: types.StringValue(createTime.Format(time.RFC3339)), + UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), + }, + expectedLabels: nil, + isValid: true, + }, + { + description: "container_parent_id_ok", + uuidContainerParentId: false, + projectResp: &resourcemanager.GetFolderDetailsResponse{ + ContainerId: utils.Ptr("cid"), + FolderId: utils.Ptr("fid"), + Labels: &map[string]string{ + "label1": "ref1", + "label2": "ref2", + }, + Parent: &resourcemanager.Parent{ + ContainerId: utils.Ptr("parent_cid"), + Id: utils.Ptr(testUUID), + }, + Name: utils.Ptr("name"), + CreationTime: &createTime, + UpdateTime: &updateTime, + }, + expected: Model{ + Id: types.StringValue("cid"), + ContainerId: types.StringValue("cid"), + FolderId: types.StringValue("fid"), + ContainerParentId: types.StringValue("parent_cid"), + Name: types.StringValue("name"), + CreationTime: types.StringValue(createTime.Format(time.RFC3339)), + UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), + }, + expectedLabels: &map[string]string{ + "label1": "ref1", + "label2": "ref2", + }, + isValid: true, + }, + { + description: "uuid_parent_id_ok", + uuidContainerParentId: true, + projectResp: &resourcemanager.GetFolderDetailsResponse{ + ContainerId: utils.Ptr("cid"), + FolderId: utils.Ptr("fid"), + Labels: &map[string]string{ + "label1": "ref1", + "label2": "ref2", + }, + Parent: &resourcemanager.Parent{ + ContainerId: utils.Ptr("parent_cid"), + Id: utils.Ptr(testUUID), // simulate UUID logic + }, + Name: utils.Ptr("name"), + CreationTime: &createTime, + UpdateTime: &updateTime, + }, + expected: Model{ + Id: types.StringValue("cid"), + ContainerId: types.StringValue("cid"), + FolderId: types.StringValue("fid"), + ContainerParentId: types.StringValue(testUUID), + Name: types.StringValue("name"), + CreationTime: types.StringValue(createTime.Format(time.RFC3339)), + UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), + }, + expectedLabels: &map[string]string{ + "label1": "ref1", + "label2": "ref2", + }, + isValid: true, + }, + { + description: "response_nil_fail", + uuidContainerParentId: false, + projectResp: nil, + expected: Model{}, + expectedLabels: nil, + isValid: false, + }, + { + description: "no_resource_id", + uuidContainerParentId: false, + projectResp: &resourcemanager.GetFolderDetailsResponse{}, + expected: Model{}, + expectedLabels: nil, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + if tt.expectedLabels == nil { + tt.expected.Labels = types.MapNull(types.StringType) + } else { + convertedLabels, err := conversion.ToTerraformStringMap(context.Background(), *tt.expectedLabels) + if err != nil { + t.Fatalf("Error converting to terraform string map: %v", err) + } + tt.expected.Labels = convertedLabels + } + var containerParentId = types.StringNull() + if tt.uuidContainerParentId { + containerParentId = types.StringValue(testUUID) + } else if tt.projectResp != nil && tt.projectResp.Parent != nil && tt.projectResp.Parent.ContainerId != nil { + containerParentId = types.StringValue(*tt.projectResp.Parent.ContainerId) + } + + model := &Model{ + ContainerId: tt.expected.ContainerId, + ContainerParentId: containerParentId, + } + + err := mapFolderFields(context.Background(), tt.projectResp, model, nil) + + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(model, &tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + input *ResourceModel + inputLabels *map[string]string + expected *resourcemanager.CreateFolderPayload + isValid bool + }{ + { + "mapping_with_conversions", + &ResourceModel{ + Model: Model{ + ContainerParentId: types.StringValue("pid"), + Name: types.StringValue("name"), + }, + OwnerEmail: types.StringValue("john.doe@stackit.cloud"), + }, + &map[string]string{ + "label1": "1", + "label2": "2", + }, + &resourcemanager.CreateFolderPayload{ + ContainerParentId: utils.Ptr("pid"), + Labels: &map[string]string{ + "label1": "1", + "label2": "2", + }, + Members: &[]resourcemanager.Member{ + { + Subject: utils.Ptr("john.doe@stackit.cloud"), + Role: utils.Ptr("owner"), + }, + }, + Name: utils.Ptr("name"), + }, + true, + }, + { + "no owner_email fails", + &ResourceModel{ + Model: Model{ + ContainerParentId: types.StringValue("pid"), + Name: types.StringValue("name"), + }, + }, + &map[string]string{}, + nil, + false, + }, + { + "nil_model", + nil, + nil, + nil, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + if tt.input != nil { + if tt.inputLabels == nil { + tt.input.Labels = types.MapNull(types.StringType) + } else { + convertedLabels, err := conversion.ToTerraformStringMap(context.Background(), *tt.inputLabels) + if err != nil { + t.Fatalf("Error converting to terraform string map: %v", err) + } + tt.input.Labels = convertedLabels + } + } + output, err := toCreatePayload(tt.input) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(output, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + input *ResourceModel + inputLabels *map[string]string + expected *resourcemanager.PartialUpdateFolderPayload + isValid bool + }{ + { + "default_ok", + &ResourceModel{}, + nil, + &resourcemanager.PartialUpdateFolderPayload{ + ContainerParentId: nil, + Labels: nil, + Name: nil, + }, + true, + }, + { + "mapping_with_conversions_ok", + &ResourceModel{ + Model: Model{ + ContainerParentId: types.StringValue("pid"), + Name: types.StringValue("name"), + }, + OwnerEmail: types.StringValue("owner_email"), + }, + &map[string]string{ + "label1": "1", + "label2": "2", + }, + &resourcemanager.PartialUpdateFolderPayload{ + ContainerParentId: utils.Ptr("pid"), + Labels: &map[string]string{ + "label1": "1", + "label2": "2", + }, + Name: utils.Ptr("name"), + }, + true, + }, + { + "nil_model", + nil, + nil, + nil, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + if tt.input != nil { + if tt.inputLabels == nil { + tt.input.Labels = types.MapNull(types.StringType) + } else { + convertedLabels, err := conversion.ToTerraformStringMap(context.Background(), *tt.inputLabels) + if err != nil { + t.Fatalf("Error converting to terraform string map: %v", err) + } + tt.input.Labels = convertedLabels + } + } + output, err := toUpdatePayload(tt.input) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(output, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToMembersPayload(t *testing.T) { + type args struct { + model *ResourceModel + } + tests := []struct { + name string + args args + want *[]resourcemanager.Member + wantErr bool + }{ + { + name: "missing model", + args: args{}, + want: nil, + wantErr: true, + }, + { + name: "empty model", + args: args{ + model: &ResourceModel{}, + }, + want: nil, + wantErr: true, + }, + { + name: "ok", + args: args{ + model: &ResourceModel{ + OwnerEmail: types.StringValue("john.doe@stackit.cloud"), + }, + }, + want: &[]resourcemanager.Member{ + { + Subject: utils.Ptr("john.doe@stackit.cloud"), + Role: utils.Ptr("owner"), + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toMembersPayload(tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toMembersPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("toMembersPayload() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/stackit/internal/services/resourcemanager/project/datasource.go b/stackit/internal/services/resourcemanager/project/datasource.go index 9977c1af2..0af3f8168 100644 --- a/stackit/internal/services/resourcemanager/project/datasource.go +++ b/stackit/internal/services/resourcemanager/project/datasource.go @@ -25,7 +25,8 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ datasource.DataSource = &projectDataSource{} + _ datasource.DataSource = &projectDataSource{} + _ datasource.DataSourceWithConfigure = &projectDataSource{} ) // NewProjectDataSource is a helper function to simplify the provider implementation. @@ -67,6 +68,8 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported", "name": "Project name.", "labels": `Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`, + "creation_time": "Date-time at which the project was created.", + "update_time": "Date-time at which the project was last modified.", } resp.Schema = schema.Schema{ @@ -122,6 +125,14 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest ), }, }, + "creation_time": schema.StringAttribute{ + Description: descriptions["creation_time"], + Computed: true, + }, + "update_time": schema.StringAttribute{ + Description: descriptions["update_time"], + Computed: true, + }, }, } } diff --git a/stackit/internal/services/resourcemanager/project/resource.go b/stackit/internal/services/resourcemanager/project/resource.go index e15733084..228c437d2 100644 --- a/stackit/internal/services/resourcemanager/project/resource.go +++ b/stackit/internal/services/resourcemanager/project/resource.go @@ -6,6 +6,7 @@ import ( "net/http" "regexp" "strings" + "time" resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" @@ -51,6 +52,8 @@ type Model struct { ContainerParentId types.String `tfsdk:"parent_container_id"` Name types.String `tfsdk:"name"` Labels types.Map `tfsdk:"labels"` + CreationTime types.String `tfsdk:"creation_time"` + UpdateTime types.String `tfsdk:"update_time"` } type ResourceModel struct { @@ -92,7 +95,7 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ "main": fmt.Sprintf("%s\n\n%s", - "Resource Manager project resource schema. To use this resource, it is required that you set the service account email in the provider configuration.", + "Resource Manager project resource schema.", "-> In case you're getting started with an empty STACKIT organization and want to use this resource to create projects in it, check out [this guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/stackit_org_service_account) for how to create a service account which you can use for authentication in the STACKIT Terraform provider.", ), "id": "Terraform's internal resource ID. It is structured as \"`container_id`\".", @@ -102,6 +105,8 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re "name": "Project name.", "labels": "Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}. \nTo create a project within a STACKIT Network Area, setting the label `networkArea=` is required. This can not be changed after project creation.", "owner_email": "Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.", + "creation_time": "Date-time at which the project was created.", + "update_time": "Date-time at which the project was last modified.", } resp.Schema = schema.Schema{ @@ -170,6 +175,14 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re Description: descriptions["owner_email"], Required: true, }, + "creation_time": schema.StringAttribute{ + Description: descriptions["creation_time"], + Computed: true, + }, + "update_time": schema.StringAttribute{ + Description: descriptions["update_time"], + Computed: true, + }, }, } } @@ -409,6 +422,8 @@ func mapProjectFields(ctx context.Context, projectResp *resourcemanager.GetProje model.ContainerId = types.StringValue(containerId) model.Name = types.StringPointerValue(projectResp.Name) model.Labels = labels + model.CreationTime = types.StringValue(projectResp.CreationTime.Format(time.RFC3339)) + model.UpdateTime = types.StringValue(projectResp.UpdateTime.Format(time.RFC3339)) if state != nil { diags := diag.Diagnostics{} @@ -418,6 +433,8 @@ func mapProjectFields(ctx context.Context, projectResp *resourcemanager.GetProje diags.Append(state.SetAttribute(ctx, path.Root("container_id"), model.ContainerId)...) diags.Append(state.SetAttribute(ctx, path.Root("name"), model.Name)...) diags.Append(state.SetAttribute(ctx, path.Root("labels"), model.Labels)...) + diags.Append(state.SetAttribute(ctx, path.Root("creation_time"), model.CreationTime)...) + diags.Append(state.SetAttribute(ctx, path.Root("update_time"), model.UpdateTime)...) if diags.HasError() { return fmt.Errorf("update terraform state: %w", core.DiagsToError(diags)) } diff --git a/stackit/internal/services/resourcemanager/project/resource_test.go b/stackit/internal/services/resourcemanager/project/resource_test.go index 2e6b49df7..28aaded68 100644 --- a/stackit/internal/services/resourcemanager/project/resource_test.go +++ b/stackit/internal/services/resourcemanager/project/resource_test.go @@ -4,6 +4,7 @@ import ( "context" "reflect" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/google/uuid" @@ -15,6 +16,10 @@ import ( func TestMapProjectFields(t *testing.T) { testUUID := uuid.New().String() + baseTime := time.Now() + createTime := baseTime + updateTime := baseTime.Add(1 * time.Hour) + tests := []struct { description string uuidContainerParentId bool @@ -24,26 +29,30 @@ func TestMapProjectFields(t *testing.T) { isValid bool }{ { - "default_ok", - false, - &resourcemanager.GetProjectResponse{ - ContainerId: utils.Ptr("cid"), - ProjectId: utils.Ptr("pid"), + description: "default_ok", + uuidContainerParentId: false, + projectResp: &resourcemanager.GetProjectResponse{ + ContainerId: utils.Ptr("cid"), + ProjectId: utils.Ptr("pid"), + CreationTime: &createTime, + UpdateTime: &updateTime, }, - Model{ + expected: Model{ Id: types.StringValue("cid"), ContainerId: types.StringValue("cid"), ProjectId: types.StringValue("pid"), ContainerParentId: types.StringNull(), Name: types.StringNull(), + CreationTime: types.StringValue(createTime.Format(time.RFC3339)), + UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), }, - nil, - true, + expectedLabels: nil, + isValid: true, }, { - "container_parent_id_ok", - false, - &resourcemanager.GetProjectResponse{ + description: "container_parent_id_ok", + uuidContainerParentId: false, + projectResp: &resourcemanager.GetProjectResponse{ ContainerId: utils.Ptr("cid"), ProjectId: utils.Ptr("pid"), Labels: &map[string]string{ @@ -54,25 +63,29 @@ func TestMapProjectFields(t *testing.T) { ContainerId: utils.Ptr("parent_cid"), Id: utils.Ptr("parent_pid"), }, - Name: utils.Ptr("name"), + Name: utils.Ptr("name"), + CreationTime: &createTime, + UpdateTime: &updateTime, }, - Model{ + expected: Model{ Id: types.StringValue("cid"), ContainerId: types.StringValue("cid"), ProjectId: types.StringValue("pid"), ContainerParentId: types.StringValue("parent_cid"), Name: types.StringValue("name"), + CreationTime: types.StringValue(createTime.Format(time.RFC3339)), + UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), }, - &map[string]string{ + expectedLabels: &map[string]string{ "label1": "ref1", "label2": "ref2", }, - true, + isValid: true, }, { - "uuid_parent_id_ok", - true, - &resourcemanager.GetProjectResponse{ + description: "uuid_parent_id_ok", + uuidContainerParentId: true, + projectResp: &resourcemanager.GetProjectResponse{ ContainerId: utils.Ptr("cid"), ProjectId: utils.Ptr("pid"), Labels: &map[string]string{ @@ -81,40 +94,45 @@ func TestMapProjectFields(t *testing.T) { }, Parent: &resourcemanager.Parent{ ContainerId: utils.Ptr("parent_cid"), - Id: utils.Ptr(testUUID), + Id: utils.Ptr(testUUID), // simulate UUID logic }, - Name: utils.Ptr("name"), + Name: utils.Ptr("name"), + CreationTime: &createTime, + UpdateTime: &updateTime, }, - Model{ + expected: Model{ Id: types.StringValue("cid"), ContainerId: types.StringValue("cid"), ProjectId: types.StringValue("pid"), ContainerParentId: types.StringValue(testUUID), Name: types.StringValue("name"), + CreationTime: types.StringValue(createTime.Format(time.RFC3339)), + UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), }, - &map[string]string{ + expectedLabels: &map[string]string{ "label1": "ref1", "label2": "ref2", }, - true, + isValid: true, }, { - "response_nil_fail", - false, - nil, - Model{}, - nil, - false, + description: "response_nil_fail", + uuidContainerParentId: false, + projectResp: nil, + expected: Model{}, + expectedLabels: nil, + isValid: false, }, { - "no_resource_id", - false, - &resourcemanager.GetProjectResponse{}, - Model{}, - nil, - false, + description: "no_resource_id", + uuidContainerParentId: false, + projectResp: &resourcemanager.GetProjectResponse{}, + expected: Model{}, + expectedLabels: nil, + isValid: false, }, } + for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { if tt.expectedLabels == nil { @@ -129,13 +147,17 @@ func TestMapProjectFields(t *testing.T) { var containerParentId = types.StringNull() if tt.uuidContainerParentId { containerParentId = types.StringValue(testUUID) + } else if tt.projectResp != nil && tt.projectResp.Parent != nil && tt.projectResp.Parent.ContainerId != nil { + containerParentId = types.StringValue(*tt.projectResp.Parent.ContainerId) } + model := &Model{ ContainerId: tt.expected.ContainerId, ContainerParentId: containerParentId, } err := mapProjectFields(context.Background(), tt.projectResp, model, nil) + if !tt.isValid && err == nil { t.Fatalf("Should have failed") } diff --git a/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go b/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go index 7fb666d22..5490aefb1 100644 --- a/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go +++ b/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go @@ -2,176 +2,450 @@ package resourcemanager_test import ( "context" + _ "embed" + "errors" "fmt" + "maps" + "sync" "testing" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/config" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -// Project resource data -var projectResource = map[string]string{ - "name": fmt.Sprintf("acc-pj-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)), - "parent_container_id": testutil.TestProjectParentContainerID, - "parent_uuid": testutil.TestProjectParentUUID, - "billing_reference": "TEST-REF", - "new_label": "a-label", -} - -func resourceConfig(name string, label *string) string { - labelConfig := "" - if label != nil { - labelConfig = fmt.Sprintf("new_label = %q", *label) - } - return fmt.Sprintf(` - %[1]s - - resource "stackit_resourcemanager_project" "parent_by_container" { - parent_container_id = "%[2]s" - name = "%[3]s" - labels = { - "billing_reference" = "%[4]s" - %[5]s - } - owner_email = "%[7]s" - } - - resource "stackit_resourcemanager_project" "parent_by_uuid" { - parent_container_id = "%[6]s" - name = "%[3]s-uuid" - owner_email = "%[7]s" - } - `, - testutil.ResourceManagerProviderConfig(), - projectResource["parent_container_id"], - name, - projectResource["billing_reference"], - labelConfig, - projectResource["parent_uuid"], - testutil.TestProjectServiceAccountEmail, - ) -} - -func TestAccResourceManagerResource(t *testing.T) { +//go:embed testdata/resource-project.tf +var resourceProject string + +//go:embed testdata/resource-folder.tf +var resourceFolder string + +var defaultLabels = config.ObjectVariable( + map[string]config.Variable{ + "env": config.StringVariable("prod"), + }, +) + +var projectNameParentContainerId = fmt.Sprintf("tfe2e-project-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) +var projectNameParentContainerIdUpdated = fmt.Sprintf("%s-updated", projectNameParentContainerId) + +var projectNameParentUUID = fmt.Sprintf("tfe2e-project-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) +var projectNameParentUUIDUpdated = fmt.Sprintf("%s-updated", projectNameParentUUID) + +var folderNameParentContainerId = fmt.Sprintf("tfe2e-folder-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) +var folderNameParentContainerIdUpdated = fmt.Sprintf("%s-updated", folderNameParentContainerId) + +var folderNameParentUUID = fmt.Sprintf("tfe2e-folder-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) +var folderNameParentUUIDUpdated = fmt.Sprintf("%s-updated", folderNameParentUUID) + +var testConfigResourceProjectParentContainerId = config.Variables{ + "name": config.StringVariable(projectNameParentContainerId), + "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), + "parent_container_id": config.StringVariable(testutil.TestProjectParentContainerID), + "labels": config.ObjectVariable( + map[string]config.Variable{ + "env": config.StringVariable("prod"), + }, + ), +} + +var testConfigResourceProjectParentUUID = config.Variables{ + "name": config.StringVariable(projectNameParentUUID), + "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), + "parent_container_id": config.StringVariable(testutil.TestProjectParentUUID), + "labels": defaultLabels, +} + +var testConfigResourceFolderParentContainerId = config.Variables{ + "name": config.StringVariable(folderNameParentContainerId), + "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), + "parent_container_id": config.StringVariable(testutil.TestProjectParentContainerID), + "labels": defaultLabels, +} + +var testConfigResourceFolderParentUUID = config.Variables{ + "name": config.StringVariable(folderNameParentUUID), + "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), + "parent_container_id": config.StringVariable(testutil.TestProjectParentUUID), + "labels": defaultLabels, +} + +func testConfigProjectNameParentContainerIdUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigResourceProjectParentContainerId)) + maps.Copy(tempConfig, testConfigResourceProjectParentContainerId) + tempConfig["name"] = config.StringVariable(projectNameParentContainerIdUpdated) + return tempConfig +} + +func testConfigProjectNameParentUUIDUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigResourceProjectParentUUID)) + maps.Copy(tempConfig, testConfigResourceProjectParentUUID) + tempConfig["name"] = config.StringVariable(projectNameParentUUIDUpdated) + return tempConfig +} + +func testConfigFolderNameParentContainerIdUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigResourceFolderParentContainerId)) + maps.Copy(tempConfig, testConfigResourceFolderParentContainerId) + tempConfig["name"] = config.StringVariable(folderNameParentContainerIdUpdated) + return tempConfig +} + +func testConfigFolderNameParentUUIDUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigResourceFolderParentUUID)) + maps.Copy(tempConfig, testConfigResourceFolderParentUUID) + tempConfig["name"] = config.StringVariable(folderNameParentUUIDUpdated) + return tempConfig +} + +func TestAccResourceManagerProjectContainerId(t *testing.T) { + t.Logf("TestAccResourceManagerProjectContainerId name: %s", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["name"])) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckResourceManagerDestroy, + CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ - // Creation + // Create { - Config: resourceConfig(projectResource["name"], nil), + ConfigVariables: testConfigResourceProjectParentContainerId, + Config: testutil.ResourceManagerProviderConfig() + resourceProject, Check: resource.ComposeAggregateTestCheckFunc( - // Parent container id project data - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.parent_by_container", "container_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.parent_by_container", "project_id"), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "name", projectResource["name"]), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "parent_container_id", projectResource["parent_container_id"]), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "labels.billing_reference", projectResource["billing_reference"]), - - // Parent UUID project data - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.parent_by_uuid", "container_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.parent_by_uuid", "project_id"), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_uuid", "name", fmt.Sprintf("%s-uuid", projectResource["name"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_uuid", "parent_container_id", projectResource["parent_uuid"]), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["parent_container_id"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "project_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "owner_email"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "update_time"), ), }, - // Data source + // Data Source { + ConfigVariables: testConfigResourceProjectParentContainerId, Config: fmt.Sprintf(` - %s + %s + %s - data "stackit_resourcemanager_project" "project_by_container" { - container_id = stackit_resourcemanager_project.parent_by_container.container_id - } - - data "stackit_resourcemanager_project" "project_by_uuid" { - project_id = stackit_resourcemanager_project.parent_by_container.project_id - } + data "stackit_resourcemanager_project" "example" { + project_id = stackit_resourcemanager_project.example.project_id + } + `, testutil.ResourceManagerProviderConfig(), resourceProject), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["name"])), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["parent_container_id"])), + resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_project.example", "container_id", "stackit_resourcemanager_project.example", "container_id"), + resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_project.example", "project_id", "stackit_resourcemanager_project.example", "project_id"), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "labels.%", "1"), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "id"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "creation_time"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "update_time"), + ), + }, + // Import + { + ConfigVariables: testConfigResourceProjectParentContainerId, + ResourceName: "stackit_resourcemanager_project.example", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return getImportIdFromID(s, "stackit_resourcemanager_project.example", "container_id") + }, + ImportStateVerifyIgnore: []string{"owner_email"}, + }, + // Update + { + ConfigVariables: testConfigProjectNameParentContainerIdUpdated(), + Config: testutil.ResourceManagerProviderConfig() + resourceProject, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigProjectNameParentContainerIdUpdated()["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["parent_container_id"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "project_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "owner_email"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "update_time"), + ), + }, + }, + }) +} - data "stackit_resourcemanager_project" "project_by_both" { - container_id = stackit_resourcemanager_project.parent_by_container.container_id - project_id = stackit_resourcemanager_project.parent_by_container.project_id - } - `, - resourceConfig(projectResource["name"], nil), +func TestAccResourceManagerProjectParentUUID(t *testing.T) { + t.Logf("TestAccResourceManagerProjectParentUUID name: %s", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["name"])) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Create + { + ConfigVariables: testConfigResourceProjectParentUUID, + Config: testutil.ResourceManagerProviderConfig() + resourceProject, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["parent_container_id"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "project_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "owner_email"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "update_time"), ), + }, + // Data Source + { + ConfigVariables: testConfigResourceProjectParentUUID, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_resourcemanager_project" "example" { + project_id = stackit_resourcemanager_project.example.project_id + } + `, testutil.ResourceManagerProviderConfig(), resourceProject), Check: resource.ComposeAggregateTestCheckFunc( - // Container project data - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_container", "id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_container", "container_id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_container", "project_id"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.project_by_container", "name", projectResource["name"]), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_container", "parent_container_id"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.project_by_container", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.project_by_container", "labels.billing_reference", projectResource["billing_reference"]), - - // UUID project data - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_uuid", "id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_uuid", "container_id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_uuid", "project_id"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.project_by_uuid", "name", projectResource["name"]), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_uuid", "parent_container_id"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.project_by_uuid", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.project_by_uuid", "labels.billing_reference", projectResource["billing_reference"]), - - // Both project data - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_both", "id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_both", "container_id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_both", "project_id"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.project_by_both", "name", projectResource["name"]), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.project_by_both", "parent_container_id"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.project_by_both", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.project_by_both", "labels.billing_reference", projectResource["billing_reference"]), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["name"])), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "labels.%", "1"), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "parent_container_id"), + resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_project.example", "container_id", "stackit_resourcemanager_project.example", "container_id"), + resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_project.example", "project_id", "stackit_resourcemanager_project.example", "project_id"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "id"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "creation_time"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "update_time"), ), }, // Import { - ResourceName: "stackit_resourcemanager_project.parent_by_container", + ConfigVariables: testConfigResourceProjectParentUUID, + ResourceName: "stackit_resourcemanager_project.example", + ImportState: true, + ImportStateVerify: true, ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_resourcemanager_project.parent_by_container"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_resourcemanager_project.parent_by_container") - } - containerId, ok := r.Primary.Attributes["container_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute container_id") - } - - return containerId, nil + return getImportIdFromID(s, "stackit_resourcemanager_project.example", "container_id") }, + ImportStateVerifyIgnore: []string{"owner_email", "parent_container_id"}, + }, + // Update + { + ConfigVariables: testConfigProjectNameParentUUIDUpdated(), + Config: testutil.ResourceManagerProviderConfig() + resourceProject, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigProjectNameParentUUIDUpdated()["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["parent_container_id"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "project_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "owner_email"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "update_time"), + ), + }, + }, + }) +} + +func TestAccResourceManagerFolderContainerId(t *testing.T) { + t.Logf("TestAccResourceManagerFolderContainerId name: %s", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["name"])) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Create + { + ConfigVariables: testConfigResourceFolderParentContainerId, + Config: testutil.ResourceManagerProviderConfigBetaEnabled() + resourceFolder, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["parent_container_id"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "folder_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "update_time"), + ), + }, + // Data Source + { + ConfigVariables: testConfigResourceFolderParentContainerId, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_resourcemanager_folder" "example" { + container_id = stackit_resourcemanager_folder.example.container_id + } + `, testutil.ResourceManagerProviderConfigBetaEnabled(), resourceFolder), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["name"])), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "labels.%", "1"), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "labels.env", "prod"), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["parent_container_id"])), + resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_folder.example", "container_id", "stackit_resourcemanager_folder.example", "container_id"), + resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_folder.example", "project_id", "stackit_resourcemanager_folder.example", "project_id"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "id"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "creation_time"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "update_time"), + ), + }, + // Import + { + ConfigVariables: testConfigResourceFolderParentContainerId, + ResourceName: "stackit_resourcemanager_folder.example", ImportState: true, ImportStateVerify: true, - // The owner_email attributes don't exist in the - // API, therefore there is no value for it during import. + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return getImportIdFromID(s, "stackit_resourcemanager_folder.example", "container_id") + }, ImportStateVerifyIgnore: []string{"owner_email"}, }, // Update { - Config: resourceConfig(fmt.Sprintf("%s-new", projectResource["name"]), utils.Ptr("a-label")), + ConfigVariables: testConfigFolderNameParentContainerIdUpdated(), + Config: testutil.ResourceManagerProviderConfigBetaEnabled() + resourceFolder, Check: resource.ComposeAggregateTestCheckFunc( - // Project data - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.parent_by_container", "container_id"), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "name", fmt.Sprintf("%s-new", projectResource["name"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "parent_container_id", projectResource["parent_container_id"]), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "labels.%", "2"), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "labels.billing_reference", projectResource["billing_reference"]), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "labels.new_label", projectResource["new_label"]), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.parent_by_container", "owner_email", testutil.TestProjectServiceAccountEmail), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigFolderNameParentContainerIdUpdated()["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigFolderNameParentContainerIdUpdated()["parent_container_id"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "owner_email", testutil.ConvertConfigVariable(testConfigFolderNameParentContainerIdUpdated()["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "folder_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "owner_email"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "update_time"), ), }, - // Deletion is done by the framework implicitly }, }) } -func testAccCheckResourceManagerDestroy(s *terraform.State) error { +func TestAccResourceManagerFolderParentUUID(t *testing.T) { + t.Logf("TestAccResourceManagerFolderParentUUID name: %s", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["name"])) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Create + { + ConfigVariables: testConfigResourceFolderParentUUID, + Config: testutil.ResourceManagerProviderConfigBetaEnabled() + resourceFolder, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigResourceFolderParentUUID["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceFolderParentUUID["parent_container_id"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceFolderParentUUID["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "folder_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "update_time"), + ), + }, + // Data Source + { + ConfigVariables: testConfigResourceFolderParentUUID, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_resourcemanager_folder" "example" { + container_id = stackit_resourcemanager_folder.example.container_id + } + `, testutil.ResourceManagerProviderConfigBetaEnabled(), resourceFolder), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigResourceFolderParentUUID["name"])), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "labels.%", "1"), + resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "parent_container_id"), + resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_folder.example", "container_id", "stackit_resourcemanager_folder.example", "container_id"), + resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_folder.example", "project_id", "stackit_resourcemanager_folder.example", "project_id"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "id"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "creation_time"), + resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "update_time"), + ), + }, + // Import + { + ConfigVariables: testConfigResourceFolderParentUUID, + ResourceName: "stackit_resourcemanager_folder.example", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + return getImportIdFromID(s, "stackit_resourcemanager_folder.example", "container_id") + }, + ImportStateVerifyIgnore: []string{"owner_email", "parent_container_id"}, + }, + // Update + { + ConfigVariables: testConfigFolderNameParentUUIDUpdated(), + Config: testutil.ResourceManagerProviderConfigBetaEnabled() + resourceFolder, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigFolderNameParentUUIDUpdated()["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigFolderNameParentUUIDUpdated()["parent_container_id"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "owner_email", testutil.ConvertConfigVariable(testConfigFolderNameParentUUIDUpdated()["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.env", "prod"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "folder_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "owner_email"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "update_time"), + ), + }, + }, + }) +} + +func testAccCheckDestroy(s *terraform.State) error { + checkFunctions := []func(s *terraform.State) error{ + testAccCheckResourceManagerProjectsDestroy, + testAccCheckResourceManagerFoldersDestroy, + } + var errs []error + + wg := sync.WaitGroup{} + wg.Add(len(checkFunctions)) + + for _, f := range checkFunctions { + go func() { + err := f(s) + if err != nil { + errs = append(errs, err) + } + wg.Done() + }() + } + wg.Wait() + return errors.Join(errs...) +} + +func testAccCheckResourceManagerProjectsDestroy(s *terraform.State) error { ctx := context.Background() var client *resourcemanager.APIClient var err error @@ -179,7 +453,7 @@ func testAccCheckResourceManagerDestroy(s *terraform.State) error { client, err = resourcemanager.NewAPIClient() } else { client, err = resourcemanager.NewAPIClient( - config.WithEndpoint(testutil.ResourceManagerCustomEndpoint), + sdkConfig.WithEndpoint(testutil.ResourceManagerCustomEndpoint), ) } if err != nil { @@ -196,7 +470,17 @@ func testAccCheckResourceManagerDestroy(s *terraform.State) error { projectsToDestroy = append(projectsToDestroy, containerId) } - projectsResp, err := client.ListProjects(ctx).ContainerParentId(projectResource["parent_container_id"]).Execute() + var containerParentId string + switch { + case testutil.TestProjectParentContainerID != "": + containerParentId = testutil.TestProjectParentContainerID + case testutil.TestProjectParentUUID != "": + containerParentId = testutil.TestProjectParentUUID + default: + return fmt.Errorf("either TestProjectParentContainerID or TestProjectParentUUID must be set") + } + + projectsResp, err := client.ListProjects(ctx).ContainerParentId(containerParentId).Execute() if err != nil { return fmt.Errorf("getting projectsResp: %w", err) } @@ -221,3 +505,69 @@ func testAccCheckResourceManagerDestroy(s *terraform.State) error { } return nil } + +func testAccCheckResourceManagerFoldersDestroy(s *terraform.State) error { + ctx := context.Background() + var client *resourcemanager.APIClient + var err error + if testutil.ResourceManagerCustomEndpoint == "" { + client, err = resourcemanager.NewAPIClient() + } else { + client, err = resourcemanager.NewAPIClient( + sdkConfig.WithEndpoint(testutil.ResourceManagerCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + foldersToDestroy := []string{} + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_resourcemanager_folder" { + continue + } + // project terraform ID: "[container_id]" + containerId := rs.Primary.ID + foldersToDestroy = append(foldersToDestroy, containerId) + } + + var containerParentId string + switch { + case testutil.TestProjectParentContainerID != "": + containerParentId = testutil.TestProjectParentContainerID + case testutil.TestProjectParentUUID != "": + containerParentId = testutil.TestProjectParentUUID + default: + return fmt.Errorf("either TestProjectParentContainerID or TestProjectParentUUID must be set") + } + + projectsResp, err := client.ListFolders(ctx).ContainerParentId(containerParentId).Execute() + if err != nil { + return fmt.Errorf("getting projectsResp: %w", err) + } + + items := *projectsResp.Items + for i := range items { + if !utils.Contains(foldersToDestroy, *items[i].ContainerId) { + continue + } + + err := client.DeleteFolder(ctx, *items[i].ContainerId).Execute() + if err != nil { + return fmt.Errorf("destroying folder %s during CheckDestroy: %w", *items[i].ContainerId, err) + } + } + return nil +} + +func getImportIdFromID(s *terraform.State, resourceName, keyName string) (string, error) { + r, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("couldn't find resource %s", resourceName) + } + id, ok := r.Primary.Attributes[keyName] + if !ok { + return "", fmt.Errorf("couldn't find attribute %s", keyName) + } + return id, nil +} diff --git a/stackit/internal/services/resourcemanager/testdata/resource-folder.tf b/stackit/internal/services/resourcemanager/testdata/resource-folder.tf new file mode 100644 index 000000000..68f9b0d0e --- /dev/null +++ b/stackit/internal/services/resourcemanager/testdata/resource-folder.tf @@ -0,0 +1,12 @@ + +variable "parent_container_id" {} +variable "name" {} +variable "labels" {} +variable "owner_email" {} + +resource "stackit_resourcemanager_folder" "example" { + parent_container_id = var.parent_container_id + name = var.name + labels = var.labels + owner_email = var.owner_email +} diff --git a/stackit/internal/services/resourcemanager/testdata/resource-project.tf b/stackit/internal/services/resourcemanager/testdata/resource-project.tf new file mode 100644 index 000000000..0de2ee190 --- /dev/null +++ b/stackit/internal/services/resourcemanager/testdata/resource-project.tf @@ -0,0 +1,12 @@ + +variable "parent_container_id" {} +variable "name" {} +variable "labels" {} +variable "owner_email" {} + +resource "stackit_resourcemanager_project" "example" { + parent_container_id = var.parent_container_id + name = var.name + labels = var.labels + owner_email = var.owner_email +} diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 25576f36d..fdfdf52ac 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -323,10 +323,8 @@ func ResourceManagerProviderConfig() string { if ResourceManagerCustomEndpoint == "" || AuthorizationCustomEndpoint == "" { return fmt.Sprintf(` provider "stackit" { - service_account_email = "%s" service_account_token = "%s" }`, - TestProjectServiceAccountEmail, token, ) } @@ -334,12 +332,35 @@ func ResourceManagerProviderConfig() string { provider "stackit" { resourcemanager_custom_endpoint = "%s" authorization_custom_endpoint = "%s" - service_account_email = "%s" service_account_token = "%s" }`, ResourceManagerCustomEndpoint, AuthorizationCustomEndpoint, - TestProjectServiceAccountEmail, + token, + ) +} + +func ResourceManagerProviderConfigBetaEnabled() string { + token := GetTestProjectServiceAccountToken("") + if ResourceManagerCustomEndpoint == "" || AuthorizationCustomEndpoint == "" { + return fmt.Sprintf(` + provider "stackit" { + service_account_token = "%s" + enable_beta_resources = true + }`, + + token, + ) + } + return fmt.Sprintf(` + provider "stackit" { + resourcemanager_custom_endpoint = "%s" + authorization_custom_endpoint = "%s" + service_account_token = "%s" + enable_beta_resources = true + }`, + ResourceManagerCustomEndpoint, + AuthorizationCustomEndpoint, token, ) } diff --git a/stackit/provider.go b/stackit/provider.go index c3ac8f336..5a938d757 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -74,6 +74,7 @@ import ( rabbitMQInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/instance" redisCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/credential" redisInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/instance" + resourceManagerFolder "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/folder" resourceManagerProject "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/project" secretsManagerInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/instance" secretsManagerUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/user" @@ -499,6 +500,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource redisInstance.NewInstanceDataSource, redisCredential.NewCredentialDataSource, resourceManagerProject.NewProjectDataSource, + resourceManagerFolder.NewFolderDataSource, secretsManagerInstance.NewInstanceDataSource, secretsManagerUser.NewUserDataSource, sqlServerFlexInstance.NewInstanceDataSource, @@ -565,6 +567,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { redisInstance.NewInstanceResource, redisCredential.NewCredentialResource, resourceManagerProject.NewProjectResource, + resourceManagerFolder.NewFolderResource, secretsManagerInstance.NewInstanceResource, secretsManagerUser.NewUserResource, sqlServerFlexInstance.NewInstanceResource,