feat(resourcemanager): add folder resource/datasource (#975)

* feat(resourcemanager): add folder resource/datasource

* feat(resourcemanager): add created_at and updated_at attributes to resourcemanager project/folder

---------

Signed-off-by: Mauritz Uphoff <mauritz.uphoff@stackit.cloud>
This commit is contained in:
Mauritz Uphoff 2025-09-17 09:53:48 +02:00 committed by GitHub
parent 27e4ef0227
commit 813b8c0e81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1844 additions and 171 deletions

View file

@ -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")
}

View file

@ -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
}

View file

@ -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)
}
})
}
}

View file

@ -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,
},
},
}
}

View file

@ -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=<networkAreaID>` 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))
}

View file

@ -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")
}

View file

@ -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",
//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"),
},
),
}
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,
)
var testConfigResourceProjectParentUUID = config.Variables{
"name": config.StringVariable(projectNameParentUUID),
"owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail),
"parent_container_id": config.StringVariable(testutil.TestProjectParentUUID),
"labels": defaultLabels,
}
func TestAccResourceManagerResource(t *testing.T) {
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" "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),
),
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(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
{
ResourceName: "stackit_resourcemanager_project.parent_by_container",
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
},
ConfigVariables: testConfigResourceProjectParentContainerId,
ResourceName: "stackit_resourcemanager_project.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_project.example", "container_id")
},
ImportStateVerifyIgnore: []string{"owner_email"},
},
// Update
{
Config: resourceConfig(fmt.Sprintf("%s-new", projectResource["name"]), utils.Ptr("a-label")),
ConfigVariables: testConfigProjectNameParentContainerIdUpdated(),
Config: testutil.ResourceManagerProviderConfig() + resourceProject,
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_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"),
),
},
// Deletion is done by the framework implicitly
},
})
}
func testAccCheckResourceManagerDestroy(s *terraform.State) error {
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(
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
{
ConfigVariables: testConfigResourceProjectParentUUID,
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", "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,
ImportStateIdFunc: func(s *terraform.State) (string, error) {
return getImportIdFromID(s, "stackit_resourcemanager_folder.example", "container_id")
},
ImportStateVerifyIgnore: []string{"owner_email"},
},
// Update
{
ConfigVariables: testConfigFolderNameParentContainerIdUpdated(),
Config: testutil.ResourceManagerProviderConfigBetaEnabled() + resourceFolder,
Check: resource.ComposeAggregateTestCheckFunc(
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"),
),
},
},
})
}
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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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,
)
}

View file

@ -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,