Feat/stackittpr 20 region adjustments | tfp (migrate first service to new regions concept) (#664)
* feat: completed bucket and credential group * feat: fix linter warnings * feat: updated documentation * feat: updated to current version of the regional api * feat: implement review findings * feat: implement further review findings * fix: make sure region is stored for the data-source in the state
This commit is contained in:
parent
c4e25f560b
commit
2923621ab0
22 changed files with 503 additions and 104 deletions
|
|
@ -26,9 +26,12 @@ data "stackit_objectstorage_bucket" "example" {
|
||||||
|
|
||||||
- `name` (String) The bucket name. It must be DNS conform.
|
- `name` (String) The bucket name. It must be DNS conform.
|
||||||
- `project_id` (String) STACKIT Project ID to which the bucket is associated.
|
- `project_id` (String) STACKIT Project ID to which the bucket is associated.
|
||||||
|
- `region` (String) The resource region. If not defined, the provider region is used.
|
||||||
|
|
||||||
### Read-Only
|
### Read-Only
|
||||||
|
|
||||||
- `id` (String) Terraform's internal data source identifier. It is structured as "`project_id`,`name`".
|
- `id` (String) Terraform's internal data source identifier. It is structured as "`project_id`,`name`".
|
||||||
- `url_path_style` (String)
|
- `url_path_style` (String)
|
||||||
- `url_virtual_hosted_style` (String)
|
- `url_virtual_hosted_style` (String)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ data "stackit_objectstorage_credentials_group" "example" {
|
||||||
- `credential_id` (String) The credential ID.
|
- `credential_id` (String) The credential ID.
|
||||||
- `credentials_group_id` (String) The credential group ID.
|
- `credentials_group_id` (String) The credential group ID.
|
||||||
- `project_id` (String) STACKIT Project ID to which the credential group is associated.
|
- `project_id` (String) STACKIT Project ID to which the credential group is associated.
|
||||||
|
- `region` (String) The resource region. If not defined, the provider region is used.
|
||||||
|
|
||||||
### Read-Only
|
### Read-Only
|
||||||
|
|
||||||
|
|
@ -36,3 +37,5 @@ data "stackit_objectstorage_credentials_group" "example" {
|
||||||
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`credentials_group_id`,`credential_id`".
|
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`credentials_group_id`,`credential_id`".
|
||||||
- `name` (String)
|
- `name` (String)
|
||||||
- `secret_access_key` (String, Sensitive)
|
- `secret_access_key` (String, Sensitive)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ data "stackit_objectstorage_credentials_group" "example" {
|
||||||
### Required
|
### Required
|
||||||
|
|
||||||
- `project_id` (String) Object Storage Project ID to which the credentials group is associated.
|
- `project_id` (String) Object Storage Project ID to which the credentials group is associated.
|
||||||
|
- `region` (String) The resource region. If not defined, the provider region is used.
|
||||||
|
|
||||||
### Optional
|
### Optional
|
||||||
|
|
||||||
|
|
@ -35,3 +36,5 @@ data "stackit_objectstorage_credentials_group" "example" {
|
||||||
|
|
||||||
- `id` (String) Terraform's internal data source identifier. It is structured as "`project_id`,`credentials_group_id`".
|
- `id` (String) Terraform's internal data source identifier. It is structured as "`project_id`,`credentials_group_id`".
|
||||||
- `urn` (String) Credentials group uniform resource name (URN)
|
- `urn` (String) Credentials group uniform resource name (URN)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,14 @@ resource "stackit_objectstorage_bucket" "example" {
|
||||||
- `name` (String) The bucket name. It must be DNS conform.
|
- `name` (String) The bucket name. It must be DNS conform.
|
||||||
- `project_id` (String) STACKIT Project ID to which the bucket is associated.
|
- `project_id` (String) STACKIT Project ID to which the bucket is associated.
|
||||||
|
|
||||||
|
### Optional
|
||||||
|
|
||||||
|
- `region` (String) The resource region. If not defined, the provider region is used.
|
||||||
|
|
||||||
### Read-Only
|
### Read-Only
|
||||||
|
|
||||||
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`name`".
|
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`name`".
|
||||||
- `url_path_style` (String)
|
- `url_path_style` (String)
|
||||||
- `url_virtual_hosted_style` (String)
|
- `url_virtual_hosted_style` (String)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ resource "stackit_objectstorage_credential" "example" {
|
||||||
### Optional
|
### Optional
|
||||||
|
|
||||||
- `expiration_timestamp` (String) Expiration timestamp, in RFC339 format without fractional seconds. Example: "2025-01-01T00:00:00Z". If not set, the credential never expires.
|
- `expiration_timestamp` (String) Expiration timestamp, in RFC339 format without fractional seconds. Example: "2025-01-01T00:00:00Z". If not set, the credential never expires.
|
||||||
|
- `region` (String) The resource region. If not defined, the provider region is used.
|
||||||
|
|
||||||
### Read-Only
|
### Read-Only
|
||||||
|
|
||||||
|
|
@ -39,3 +40,5 @@ resource "stackit_objectstorage_credential" "example" {
|
||||||
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`credentials_group_id`,`credential_id`".
|
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`credentials_group_id`,`credential_id`".
|
||||||
- `name` (String)
|
- `name` (String)
|
||||||
- `secret_access_key` (String, Sensitive)
|
- `secret_access_key` (String, Sensitive)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,14 @@ resource "stackit_objectstorage_credentials_group" "example" {
|
||||||
- `name` (String) The credentials group's display name.
|
- `name` (String) The credentials group's display name.
|
||||||
- `project_id` (String) Project ID to which the credentials group is associated.
|
- `project_id` (String) Project ID to which the credentials group is associated.
|
||||||
|
|
||||||
|
### Optional
|
||||||
|
|
||||||
|
- `region` (String) The resource region. If not defined, the provider region is used.
|
||||||
|
|
||||||
### Read-Only
|
### Read-Only
|
||||||
|
|
||||||
- `credentials_group_id` (String) The credentials group ID
|
- `credentials_group_id` (String) The credentials group ID
|
||||||
- `id` (String) Terraform's internal data source identifier. It is structured as "`project_id`,`credentials_group_id`".
|
- `id` (String) Terraform's internal data source identifier. It is structured as "`project_id`,`credentials_group_id`".
|
||||||
- `urn` (String) Credentials group uniform resource name (URN)
|
- `urn` (String) Credentials group uniform resource name (URN)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -19,7 +19,7 @@ require (
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.2
|
github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.2
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.1
|
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.1
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.17.0
|
github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.17.0
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.11.1
|
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.0.0
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.1
|
github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.1
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.19.1
|
github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.19.1
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.17.0
|
github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.17.0
|
||||||
|
|
|
||||||
6
go.sum
6
go.sum
|
|
@ -153,6 +153,8 @@ github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L
|
||||||
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||||
github.com/stackitcloud/stackit-sdk-go/core v0.15.1 h1:hIj/k/JXEuYmud3VWo3lr7Cfj2hfl4gG9nUIzcaZ9pM=
|
github.com/stackitcloud/stackit-sdk-go/core v0.15.1 h1:hIj/k/JXEuYmud3VWo3lr7Cfj2hfl4gG9nUIzcaZ9pM=
|
||||||
github.com/stackitcloud/stackit-sdk-go/core v0.15.1/go.mod h1:mDX1mSTsB3mP+tNBGcFNx6gH1mGBN4T+dVt+lcw7nlw=
|
github.com/stackitcloud/stackit-sdk-go/core v0.15.1/go.mod h1:mDX1mSTsB3mP+tNBGcFNx6gH1mGBN4T+dVt+lcw7nlw=
|
||||||
|
github.com/stackitcloud/stackit-sdk-go/core v0.15.2-0.20250204115447-63b21f25e380 h1:qy9kkEavIBFi11ztFnj6w1suZtzSnXUAo0bMwqDrLJQ=
|
||||||
|
github.com/stackitcloud/stackit-sdk-go/core v0.15.2-0.20250204115447-63b21f25e380/go.mod h1:mDX1mSTsB3mP+tNBGcFNx6gH1mGBN4T+dVt+lcw7nlw=
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/argus v0.11.0 h1:JVEx/ouHB6PlwGzQa3ywyDym1HTWo3WgrxAyXprCnuM=
|
github.com/stackitcloud/stackit-sdk-go/services/argus v0.11.0 h1:JVEx/ouHB6PlwGzQa3ywyDym1HTWo3WgrxAyXprCnuM=
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/argus v0.11.0/go.mod h1:nVllQfYODhX1q3bgwVTLO7wHOp+8NMLiKbn3u/Dg5nU=
|
github.com/stackitcloud/stackit-sdk-go/services/argus v0.11.0/go.mod h1:nVllQfYODhX1q3bgwVTLO7wHOp+8NMLiKbn3u/Dg5nU=
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.5.1 h1:MAB7z8Hr6nCNdPCiLy5uNOJB+1R/eYFseFNEQUYK7qc=
|
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.5.1 h1:MAB7z8Hr6nCNdPCiLy5uNOJB+1R/eYFseFNEQUYK7qc=
|
||||||
|
|
@ -169,8 +171,8 @@ github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.1 h1:J+GLgfDIDnNpq
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.1/go.mod h1:nuZK6OXyZ4zlGsC1gZDj9+ajJzzFi9vVgSSRQlEJAqA=
|
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.1/go.mod h1:nuZK6OXyZ4zlGsC1gZDj9+ajJzzFi9vVgSSRQlEJAqA=
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.17.0 h1:SXNkKaAsGOkr9C6bv2i7q3kucxL3kril+z2wnshlXK0=
|
github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.17.0 h1:SXNkKaAsGOkr9C6bv2i7q3kucxL3kril+z2wnshlXK0=
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.17.0/go.mod h1:5hMtm08NrL+QcgKl94zUDrY7VEzKRcvCJOEOvENBxqc=
|
github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.17.0/go.mod h1:5hMtm08NrL+QcgKl94zUDrY7VEzKRcvCJOEOvENBxqc=
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.11.1 h1:Df3fTAHaVgyiiyp9LyTTQI8jXSVeGo49eW5ya4AATCY=
|
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.0.0 h1:/0n2zcH1nMw2noroGhz0fgu2YqtNo9v3AsVhXMRtmtw=
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.11.1/go.mod h1:V2LEHKyTaaiEBi9L3v62mNQ7xyJSred4OK+himLJOZQ=
|
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.0.0/go.mod h1:0XumGX33DT6ItyD8yMlogSPWvpIuoqN7RZBrpUBPX+k=
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.1 h1:sIz4wJIz6/9Eh6nSoi2sQ+Ef53iOrFsqLKIp2oRkmgo=
|
github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.1 h1:sIz4wJIz6/9Eh6nSoi2sQ+Ef53iOrFsqLKIp2oRkmgo=
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.1/go.mod h1:okcRTrNDTI3d7MQcYJMliK0qoXeLq0b1wvZuEqgJIWE=
|
github.com/stackitcloud/stackit-sdk-go/services/observability v0.2.1/go.mod h1:okcRTrNDTI3d7MQcYJMliK0qoXeLq0b1wvZuEqgJIWE=
|
||||||
github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.19.1 h1:hwRkCCUSWMhKTc7fLakL89V6+9xkxsFQlRthVmrvi1U=
|
github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.19.1 h1:hwRkCCUSWMhKTc7fLakL89V6+9xkxsFQlRthVmrvi1U=
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||||
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
|
@ -29,7 +30,8 @@ func NewBucketDataSource() datasource.DataSource {
|
||||||
|
|
||||||
// bucketDataSource is the data source implementation.
|
// bucketDataSource is the data source implementation.
|
||||||
type bucketDataSource struct {
|
type bucketDataSource struct {
|
||||||
client *objectstorage.APIClient
|
client *objectstorage.APIClient
|
||||||
|
providerData core.ProviderData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata returns the data source type name.
|
// Metadata returns the data source type name.
|
||||||
|
|
@ -44,7 +46,8 @@ func (r *bucketDataSource) Configure(ctx context.Context, req datasource.Configu
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
var ok bool
|
||||||
|
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||||
if !ok {
|
if !ok {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||||
return
|
return
|
||||||
|
|
@ -52,15 +55,14 @@ func (r *bucketDataSource) Configure(ctx context.Context, req datasource.Configu
|
||||||
|
|
||||||
var apiClient *objectstorage.APIClient
|
var apiClient *objectstorage.APIClient
|
||||||
var err error
|
var err error
|
||||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithRegion(providerData.Region),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,6 +84,7 @@ func (r *bucketDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
|
||||||
"project_id": "STACKIT Project ID to which the bucket is associated.",
|
"project_id": "STACKIT Project ID to which the bucket is associated.",
|
||||||
"url_path_style": "URL in path style.",
|
"url_path_style": "URL in path style.",
|
||||||
"url_virtual_hosted_style": "URL in virtual hosted style.",
|
"url_virtual_hosted_style": "URL in virtual hosted style.",
|
||||||
|
"region": "The resource region. If not defined, the provider region is used.",
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
|
|
@ -112,6 +115,11 @@ func (r *bucketDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
|
||||||
"url_virtual_hosted_style": schema.StringAttribute{
|
"url_virtual_hosted_style": schema.StringAttribute{
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
"region": schema.StringAttribute{
|
||||||
|
// the region cannot be found automatically, so it has to be passed
|
||||||
|
Optional: true,
|
||||||
|
Description: descriptions["region"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,10 +134,18 @@ func (r *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest,
|
||||||
}
|
}
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
bucketName := model.Name.ValueString()
|
bucketName := model.Name.ValueString()
|
||||||
|
var region string
|
||||||
|
if utils.IsUndefined(model.Region) {
|
||||||
|
region = r.providerData.Region
|
||||||
|
} else {
|
||||||
|
region = model.Region.ValueString()
|
||||||
|
}
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "name", bucketName)
|
ctx = tflog.SetField(ctx, "name", bucketName)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
bucketResp, err := r.client.GetBucket(ctx, projectId, bucketName).Execute()
|
bucketResp, err := r.client.GetBucket(ctx, projectId, region, bucketName).Execute()
|
||||||
if err != nil {
|
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
|
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.StatusNotFound {
|
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||||
|
|
@ -140,7 +156,7 @@ func (r *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map response body to schema
|
// Map response body to schema
|
||||||
err = mapFields(bucketResp, &model)
|
err = mapFields(bucketResp, &model, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Processing API payload: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Processing API payload: %v", err))
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||||
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
|
@ -28,6 +29,7 @@ var (
|
||||||
_ resource.Resource = &bucketResource{}
|
_ resource.Resource = &bucketResource{}
|
||||||
_ resource.ResourceWithConfigure = &bucketResource{}
|
_ resource.ResourceWithConfigure = &bucketResource{}
|
||||||
_ resource.ResourceWithImportState = &bucketResource{}
|
_ resource.ResourceWithImportState = &bucketResource{}
|
||||||
|
_ resource.ResourceWithModifyPlan = &bucketResource{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
|
|
@ -36,6 +38,7 @@ type Model struct {
|
||||||
ProjectId types.String `tfsdk:"project_id"`
|
ProjectId types.String `tfsdk:"project_id"`
|
||||||
URLPathStyle types.String `tfsdk:"url_path_style"`
|
URLPathStyle types.String `tfsdk:"url_path_style"`
|
||||||
URLVirtualHostedStyle types.String `tfsdk:"url_virtual_hosted_style"`
|
URLVirtualHostedStyle types.String `tfsdk:"url_virtual_hosted_style"`
|
||||||
|
Region types.String `tfsdk:"region"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBucketResource is a helper function to simplify the provider implementation.
|
// NewBucketResource is a helper function to simplify the provider implementation.
|
||||||
|
|
@ -45,7 +48,38 @@ func NewBucketResource() resource.Resource {
|
||||||
|
|
||||||
// bucketResource is the resource implementation.
|
// bucketResource is the resource implementation.
|
||||||
type bucketResource struct {
|
type bucketResource struct {
|
||||||
client *objectstorage.APIClient
|
client *objectstorage.APIClient
|
||||||
|
providerData core.ProviderData
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModifyPlan implements resource.ResourceWithModifyPlan.
|
||||||
|
// Use the modifier to set the effective region in the current plan.
|
||||||
|
func (r *bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
|
||||||
|
var configModel Model
|
||||||
|
// skip initial empty configuration to avoid follow-up errors
|
||||||
|
if req.Config.Raw.IsNull() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var planModel Model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.Region, resp)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata returns the resource type name.
|
// Metadata returns the resource type name.
|
||||||
|
|
@ -60,7 +94,8 @@ func (r *bucketResource) Configure(ctx context.Context, req resource.ConfigureRe
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
var ok bool
|
||||||
|
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||||
if !ok {
|
if !ok {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||||
return
|
return
|
||||||
|
|
@ -68,15 +103,14 @@ func (r *bucketResource) Configure(ctx context.Context, req resource.ConfigureRe
|
||||||
|
|
||||||
var apiClient *objectstorage.APIClient
|
var apiClient *objectstorage.APIClient
|
||||||
var err error
|
var err error
|
||||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithRegion(providerData.Region),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,6 +132,7 @@ func (r *bucketResource) Schema(_ context.Context, _ resource.SchemaRequest, res
|
||||||
"project_id": "STACKIT Project ID to which the bucket is associated.",
|
"project_id": "STACKIT Project ID to which the bucket is associated.",
|
||||||
"url_path_style": "URL in path style.",
|
"url_path_style": "URL in path style.",
|
||||||
"url_virtual_hosted_style": "URL in virtual hosted style.",
|
"url_virtual_hosted_style": "URL in virtual hosted style.",
|
||||||
|
"region": "The resource region. If not defined, the provider region is used.",
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
|
|
@ -139,6 +174,15 @@ func (r *bucketResource) Schema(_ context.Context, _ resource.SchemaRequest, res
|
||||||
"url_virtual_hosted_style": schema.StringAttribute{
|
"url_virtual_hosted_style": schema.StringAttribute{
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
"region": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
// must be computed to allow for storing the override value from the provider
|
||||||
|
Computed: true,
|
||||||
|
Description: descriptions["region"],
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -153,31 +197,34 @@ func (r *bucketResource) Create(ctx context.Context, req resource.CreateRequest,
|
||||||
}
|
}
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
bucketName := model.Name.ValueString()
|
bucketName := model.Name.ValueString()
|
||||||
|
region := model.Region.ValueString()
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "name", bucketName)
|
ctx = tflog.SetField(ctx, "name", bucketName)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
// Handle project init
|
// Handle project init
|
||||||
err := enableProject(ctx, &model, r.client)
|
err := enableProject(ctx, &model, region, r.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Enabling object storage project before creation: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Enabling object storage project before creation: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new bucket
|
// Create new bucket
|
||||||
_, err = r.client.CreateBucket(ctx, projectId, bucketName).Execute()
|
_, err = r.client.CreateBucket(ctx, projectId, region, bucketName).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Calling API: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Calling API: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
waitResp, err := wait.CreateBucketWaitHandler(ctx, r.client, projectId, bucketName).WaitWithContext(ctx)
|
waitResp, err := wait.CreateBucketWaitHandler(ctx, r.client, projectId, region, bucketName).WaitWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Bucket creation waiting: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Bucket creation waiting: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map response body to schema
|
// Map response body to schema
|
||||||
err = mapFields(waitResp, &model)
|
err = mapFields(waitResp, &model, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Processing API payload: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Processing API payload: %v", err))
|
||||||
return
|
return
|
||||||
|
|
@ -200,10 +247,13 @@ func (r *bucketResource) Read(ctx context.Context, req resource.ReadRequest, res
|
||||||
}
|
}
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
bucketName := model.Name.ValueString()
|
bucketName := model.Name.ValueString()
|
||||||
|
region := model.Region.ValueString()
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "name", bucketName)
|
ctx = tflog.SetField(ctx, "name", bucketName)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
bucketResp, err := r.client.GetBucket(ctx, projectId, bucketName).Execute()
|
bucketResp, err := r.client.GetBucket(ctx, projectId, region, bucketName).Execute()
|
||||||
if err != nil {
|
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
|
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.StatusNotFound {
|
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||||
|
|
@ -215,7 +265,7 @@ func (r *bucketResource) Read(ctx context.Context, req resource.ReadRequest, res
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map response body to schema
|
// Map response body to schema
|
||||||
err = mapFields(bucketResp, &model)
|
err = mapFields(bucketResp, &model, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Processing API payload: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Processing API payload: %v", err))
|
||||||
return
|
return
|
||||||
|
|
@ -246,15 +296,18 @@ func (r *bucketResource) Delete(ctx context.Context, req resource.DeleteRequest,
|
||||||
}
|
}
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
bucketName := model.Name.ValueString()
|
bucketName := model.Name.ValueString()
|
||||||
|
region := model.Region.ValueString()
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "name", bucketName)
|
ctx = tflog.SetField(ctx, "name", bucketName)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
// Delete existing bucket
|
// Delete existing bucket
|
||||||
_, err := r.client.DeleteBucket(ctx, projectId, bucketName).Execute()
|
_, err := r.client.DeleteBucket(ctx, projectId, region, bucketName).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", fmt.Sprintf("Calling API: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", fmt.Sprintf("Calling API: %v", err))
|
||||||
}
|
}
|
||||||
_, err = wait.DeleteBucketWaitHandler(ctx, r.client, projectId, bucketName).WaitWithContext(ctx)
|
_, err = wait.DeleteBucketWaitHandler(ctx, r.client, projectId, region, bucketName).WaitWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", fmt.Sprintf("Bucket deletion waiting: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", fmt.Sprintf("Bucket deletion waiting: %v", err))
|
||||||
return
|
return
|
||||||
|
|
@ -279,7 +332,7 @@ func (r *bucketResource) ImportState(ctx context.Context, req resource.ImportSta
|
||||||
tflog.Info(ctx, "ObjectStorage bucket state imported")
|
tflog.Info(ctx, "ObjectStorage bucket state imported")
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapFields(bucketResp *objectstorage.GetBucketResponse, model *Model) error {
|
func mapFields(bucketResp *objectstorage.GetBucketResponse, model *Model, region string) error {
|
||||||
if bucketResp == nil {
|
if bucketResp == nil {
|
||||||
return fmt.Errorf("response input is nil")
|
return fmt.Errorf("response input is nil")
|
||||||
}
|
}
|
||||||
|
|
@ -300,19 +353,20 @@ func mapFields(bucketResp *objectstorage.GetBucketResponse, model *Model) error
|
||||||
)
|
)
|
||||||
model.URLPathStyle = types.StringPointerValue(bucket.UrlPathStyle)
|
model.URLPathStyle = types.StringPointerValue(bucket.UrlPathStyle)
|
||||||
model.URLVirtualHostedStyle = types.StringPointerValue(bucket.UrlVirtualHostedStyle)
|
model.URLVirtualHostedStyle = types.StringPointerValue(bucket.UrlVirtualHostedStyle)
|
||||||
|
model.Region = types.StringValue(region)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type objectStorageClient interface {
|
type objectStorageClient interface {
|
||||||
EnableServiceExecute(ctx context.Context, projectId string) (*objectstorage.ProjectStatus, error)
|
EnableServiceExecute(ctx context.Context, projectId, region string) (*objectstorage.ProjectStatus, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens
|
// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens
|
||||||
func enableProject(ctx context.Context, model *Model, client objectStorageClient) error {
|
func enableProject(ctx context.Context, model *Model, region string, client objectStorageClient) error {
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
|
|
||||||
// From the object storage OAS: Creation will also be successful if the project is already enabled, but will not create a duplicate
|
// From the object storage OAS: Creation will also be successful if the project is already enabled, but will not create a duplicate
|
||||||
_, err := client.EnableServiceExecute(ctx, projectId)
|
_, err := client.EnableServiceExecute(ctx, projectId, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create object storage project: %w", err)
|
return fmt.Errorf("failed to create object storage project: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package objectstorage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ type objectStorageClientMocked struct {
|
||||||
returnError bool
|
returnError bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, projectId string) (*objectstorage.ProjectStatus, error) {
|
func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, projectId, _ string) (*objectstorage.ProjectStatus, error) {
|
||||||
if c.returnError {
|
if c.returnError {
|
||||||
return nil, fmt.Errorf("create project failed")
|
return nil, fmt.Errorf("create project failed")
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +44,7 @@ func TestMapFields(t *testing.T) {
|
||||||
ProjectId: types.StringValue("pid"),
|
ProjectId: types.StringValue("pid"),
|
||||||
URLPathStyle: types.StringNull(),
|
URLPathStyle: types.StringNull(),
|
||||||
URLVirtualHostedStyle: types.StringNull(),
|
URLVirtualHostedStyle: types.StringNull(),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
@ -60,6 +62,7 @@ func TestMapFields(t *testing.T) {
|
||||||
ProjectId: types.StringValue("pid"),
|
ProjectId: types.StringValue("pid"),
|
||||||
URLPathStyle: types.StringValue("url/path/style"),
|
URLPathStyle: types.StringValue("url/path/style"),
|
||||||
URLVirtualHostedStyle: types.StringValue("url/virtual/hosted/style"),
|
URLVirtualHostedStyle: types.StringValue("url/virtual/hosted/style"),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
@ -77,6 +80,7 @@ func TestMapFields(t *testing.T) {
|
||||||
ProjectId: types.StringValue("pid"),
|
ProjectId: types.StringValue("pid"),
|
||||||
URLPathStyle: types.StringValue(""),
|
URLPathStyle: types.StringValue(""),
|
||||||
URLVirtualHostedStyle: types.StringValue(""),
|
URLVirtualHostedStyle: types.StringValue(""),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
@ -99,7 +103,7 @@ func TestMapFields(t *testing.T) {
|
||||||
ProjectId: tt.expected.ProjectId,
|
ProjectId: tt.expected.ProjectId,
|
||||||
Name: tt.expected.Name,
|
Name: tt.expected.Name,
|
||||||
}
|
}
|
||||||
err := mapFields(tt.input, model)
|
err := mapFields(tt.input, model, "eu01")
|
||||||
if !tt.isValid && err == nil {
|
if !tt.isValid && err == nil {
|
||||||
t.Fatalf("Should have failed")
|
t.Fatalf("Should have failed")
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +142,7 @@ func TestEnableProject(t *testing.T) {
|
||||||
client := &objectStorageClientMocked{
|
client := &objectStorageClientMocked{
|
||||||
returnError: tt.enableFails,
|
returnError: tt.enableFails,
|
||||||
}
|
}
|
||||||
err := enableProject(context.Background(), &Model{}, client)
|
err := enableProject(context.Background(), &Model{}, "eu01", client)
|
||||||
if !tt.isValid && err == nil {
|
if !tt.isValid && err == nil {
|
||||||
t.Fatalf("Should have failed")
|
t.Fatalf("Should have failed")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||||
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
||||||
|
|
@ -25,7 +26,8 @@ func NewCredentialDataSource() datasource.DataSource {
|
||||||
|
|
||||||
// credentialDataSource is the resource implementation.
|
// credentialDataSource is the resource implementation.
|
||||||
type credentialDataSource struct {
|
type credentialDataSource struct {
|
||||||
client *objectstorage.APIClient
|
client *objectstorage.APIClient
|
||||||
|
providerData core.ProviderData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata returns the resource type name.
|
// Metadata returns the resource type name.
|
||||||
|
|
@ -40,7 +42,8 @@ func (r *credentialDataSource) Configure(ctx context.Context, req datasource.Con
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
var ok bool
|
||||||
|
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||||
if !ok {
|
if !ok {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||||
return
|
return
|
||||||
|
|
@ -48,15 +51,14 @@ func (r *credentialDataSource) Configure(ctx context.Context, req datasource.Con
|
||||||
|
|
||||||
var apiClient *objectstorage.APIClient
|
var apiClient *objectstorage.APIClient
|
||||||
var err error
|
var err error
|
||||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithRegion(providerData.Region),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,6 +79,7 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ
|
||||||
"credential_id": "The credential ID.",
|
"credential_id": "The credential ID.",
|
||||||
"credentials_group_id": "The credential group ID.",
|
"credentials_group_id": "The credential group ID.",
|
||||||
"project_id": "STACKIT Project ID to which the credential group is associated.",
|
"project_id": "STACKIT Project ID to which the credential group is associated.",
|
||||||
|
"region": "The resource region. If not defined, the provider region is used.",
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
|
|
@ -111,6 +114,11 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ
|
||||||
"expiration_timestamp": schema.StringAttribute{
|
"expiration_timestamp": schema.StringAttribute{
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
"region": schema.StringAttribute{
|
||||||
|
// the region cannot be found automatically, so it has to be passed
|
||||||
|
Optional: true,
|
||||||
|
Description: descriptions["region"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,11 +135,19 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||||
credentialId := model.CredentialId.ValueString()
|
credentialId := model.CredentialId.ValueString()
|
||||||
|
var region string
|
||||||
|
if utils.IsUndefined(model.Region) {
|
||||||
|
region = r.providerData.Region
|
||||||
|
} else {
|
||||||
|
region = model.Region.ValueString()
|
||||||
|
}
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||||
ctx = tflog.SetField(ctx, "credential_id", credentialId)
|
ctx = tflog.SetField(ctx, "credential_id", credentialId)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
found, err := readCredentials(ctx, &model, r.client)
|
found, err := readCredentials(ctx, &model, region, r.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err))
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
|
"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/core"
|
||||||
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
|
@ -29,6 +30,7 @@ var (
|
||||||
_ resource.Resource = &credentialResource{}
|
_ resource.Resource = &credentialResource{}
|
||||||
_ resource.ResourceWithConfigure = &credentialResource{}
|
_ resource.ResourceWithConfigure = &credentialResource{}
|
||||||
_ resource.ResourceWithImportState = &credentialResource{}
|
_ resource.ResourceWithImportState = &credentialResource{}
|
||||||
|
_ resource.ResourceWithModifyPlan = &credentialResource{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
|
|
@ -40,6 +42,7 @@ type Model struct {
|
||||||
AccessKey types.String `tfsdk:"access_key"`
|
AccessKey types.String `tfsdk:"access_key"`
|
||||||
SecretAccessKey types.String `tfsdk:"secret_access_key"`
|
SecretAccessKey types.String `tfsdk:"secret_access_key"`
|
||||||
ExpirationTimestamp types.String `tfsdk:"expiration_timestamp"`
|
ExpirationTimestamp types.String `tfsdk:"expiration_timestamp"`
|
||||||
|
Region types.String `tfsdk:"region"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCredentialResource is a helper function to simplify the provider implementation.
|
// NewCredentialResource is a helper function to simplify the provider implementation.
|
||||||
|
|
@ -49,7 +52,38 @@ func NewCredentialResource() resource.Resource {
|
||||||
|
|
||||||
// credentialResource is the resource implementation.
|
// credentialResource is the resource implementation.
|
||||||
type credentialResource struct {
|
type credentialResource struct {
|
||||||
client *objectstorage.APIClient
|
client *objectstorage.APIClient
|
||||||
|
providerData core.ProviderData
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModifyPlan implements resource.ResourceWithModifyPlan.
|
||||||
|
// Use the modifier to set the effective region in the current plan.
|
||||||
|
func (r *credentialResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
|
||||||
|
var configModel Model
|
||||||
|
// skip initial empty configuration to avoid follow-up errors
|
||||||
|
if req.Config.Raw.IsNull() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var planModel Model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.Region, resp)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata returns the resource type name.
|
// Metadata returns the resource type name.
|
||||||
|
|
@ -64,7 +98,8 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
var ok bool
|
||||||
|
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||||
if !ok {
|
if !ok {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||||
return
|
return
|
||||||
|
|
@ -72,15 +107,14 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu
|
||||||
|
|
||||||
var apiClient *objectstorage.APIClient
|
var apiClient *objectstorage.APIClient
|
||||||
var err error
|
var err error
|
||||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithRegion(providerData.Region),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,6 +136,7 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest,
|
||||||
"credentials_group_id": "The credential group ID.",
|
"credentials_group_id": "The credential group ID.",
|
||||||
"project_id": "STACKIT Project ID to which the credential group is associated.",
|
"project_id": "STACKIT Project ID to which the credential group is associated.",
|
||||||
"expiration_timestamp": "Expiration timestamp, in RFC339 format without fractional seconds. Example: \"2025-01-01T00:00:00Z\". If not set, the credential never expires.",
|
"expiration_timestamp": "Expiration timestamp, in RFC339 format without fractional seconds. Example: \"2025-01-01T00:00:00Z\". If not set, the credential never expires.",
|
||||||
|
"region": "The resource region. If not defined, the provider region is used.",
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
|
|
@ -169,6 +204,15 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest,
|
||||||
stringplanmodifier.UseStateForUnknown(),
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"region": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
// must be computed to allow for storing the override value from the provider
|
||||||
|
Computed: true,
|
||||||
|
Description: descriptions["region"],
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -183,11 +227,14 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ
|
||||||
}
|
}
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||||
|
region := model.Region.ValueString()
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
// Handle project init
|
// Handle project init
|
||||||
err := enableProject(ctx, &model, r.client)
|
err := enableProject(ctx, &model, region, r.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Enabling object storage project before creation: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Enabling object storage project before creation: %v", err))
|
||||||
return
|
return
|
||||||
|
|
@ -200,7 +247,7 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Create new credential
|
// Create new credential
|
||||||
credentialResp, err := r.client.CreateAccessKey(ctx, projectId).CredentialsGroup(credentialsGroupId).CreateAccessKeyPayload(*payload).Execute()
|
credentialResp, err := r.client.CreateAccessKey(ctx, projectId, region).CredentialsGroup(credentialsGroupId).CreateAccessKeyPayload(*payload).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err))
|
||||||
return
|
return
|
||||||
|
|
@ -213,7 +260,7 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ
|
||||||
ctx = tflog.SetField(ctx, "credential_id", credentialId)
|
ctx = tflog.SetField(ctx, "credential_id", credentialId)
|
||||||
|
|
||||||
// Map response body to schema
|
// Map response body to schema
|
||||||
err = mapFields(credentialResp, &model)
|
err = mapFields(credentialResp, &model, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err))
|
||||||
return
|
return
|
||||||
|
|
@ -238,11 +285,14 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest,
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||||
credentialId := model.CredentialId.ValueString()
|
credentialId := model.CredentialId.ValueString()
|
||||||
|
region := model.Region.ValueString()
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||||
ctx = tflog.SetField(ctx, "credential_id", credentialId)
|
ctx = tflog.SetField(ctx, "credential_id", credentialId)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
found, err := readCredentials(ctx, &model, r.client)
|
found, err := readCredentials(ctx, &model, region, r.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err))
|
||||||
return
|
return
|
||||||
|
|
@ -279,12 +329,15 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||||
credentialId := model.CredentialId.ValueString()
|
credentialId := model.CredentialId.ValueString()
|
||||||
|
region := model.Region.ValueString()
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||||
ctx = tflog.SetField(ctx, "credential_id", credentialId)
|
ctx = tflog.SetField(ctx, "credential_id", credentialId)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
// Delete existing credential
|
// Delete existing credential
|
||||||
_, err := r.client.DeleteAccessKey(ctx, projectId, credentialId).CredentialsGroup(credentialsGroupId).Execute()
|
_, err := r.client.DeleteAccessKey(ctx, projectId, region, credentialId).CredentialsGroup(credentialsGroupId).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err))
|
||||||
}
|
}
|
||||||
|
|
@ -311,15 +364,15 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor
|
||||||
}
|
}
|
||||||
|
|
||||||
type objectStorageClient interface {
|
type objectStorageClient interface {
|
||||||
EnableServiceExecute(ctx context.Context, projectId string) (*objectstorage.ProjectStatus, error)
|
EnableServiceExecute(ctx context.Context, projectId, region string) (*objectstorage.ProjectStatus, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens
|
// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens
|
||||||
func enableProject(ctx context.Context, model *Model, client objectStorageClient) error {
|
func enableProject(ctx context.Context, model *Model, region string, client objectStorageClient) error {
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
|
|
||||||
// From the object storage OAS: Creation will also be successful if the project is already enabled, but will not create a duplicate
|
// From the object storage OAS: Creation will also be successful if the project is already enabled, but will not create a duplicate
|
||||||
_, err := client.EnableServiceExecute(ctx, projectId)
|
_, err := client.EnableServiceExecute(ctx, projectId, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create object storage project: %w", err)
|
return fmt.Errorf("failed to create object storage project: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -348,7 +401,7 @@ func toCreatePayload(model *Model) (*objectstorage.CreateAccessKeyPayload, error
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapFields(credentialResp *objectstorage.CreateAccessKeyResponse, model *Model) error {
|
func mapFields(credentialResp *objectstorage.CreateAccessKeyResponse, model *Model, region string) error {
|
||||||
if credentialResp == nil {
|
if credentialResp == nil {
|
||||||
return fmt.Errorf("response input is nil")
|
return fmt.Errorf("response input is nil")
|
||||||
}
|
}
|
||||||
|
|
@ -389,18 +442,19 @@ func mapFields(credentialResp *objectstorage.CreateAccessKeyResponse, model *Mod
|
||||||
model.Name = types.StringPointerValue(credentialResp.DisplayName)
|
model.Name = types.StringPointerValue(credentialResp.DisplayName)
|
||||||
model.AccessKey = types.StringPointerValue(credentialResp.AccessKey)
|
model.AccessKey = types.StringPointerValue(credentialResp.AccessKey)
|
||||||
model.SecretAccessKey = types.StringPointerValue(credentialResp.SecretAccessKey)
|
model.SecretAccessKey = types.StringPointerValue(credentialResp.SecretAccessKey)
|
||||||
|
model.Region = types.StringValue(region)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readCredentials gets all the existing credentials for the specified credentials group,
|
// readCredentials gets all the existing credentials for the specified credentials group,
|
||||||
// finds the credential that is being read and updates the state.
|
// finds the credential that is being read and updates the state.
|
||||||
// Returns True if the credential was found, False otherwise.
|
// Returns True if the credential was found, False otherwise.
|
||||||
func readCredentials(ctx context.Context, model *Model, client *objectstorage.APIClient) (bool, error) {
|
func readCredentials(ctx context.Context, model *Model, region string, client *objectstorage.APIClient) (bool, error) {
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||||
credentialId := model.CredentialId.ValueString()
|
credentialId := model.CredentialId.ValueString()
|
||||||
|
|
||||||
credentialsGroupResp, err := client.ListAccessKeys(ctx, projectId).CredentialsGroup(credentialsGroupId).Execute()
|
credentialsGroupResp, err := client.ListAccessKeys(ctx, projectId, region).CredentialsGroup(credentialsGroupId).Execute()
|
||||||
if err != nil {
|
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
|
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.StatusNotFound {
|
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||||
|
|
@ -443,6 +497,7 @@ func readCredentials(ctx context.Context, model *Model, client *objectstorage.AP
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
model.Region = types.StringValue(region)
|
||||||
|
|
||||||
return foundCredential, nil
|
return foundCredential, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ type objectStorageClientMocked struct {
|
||||||
returnError bool
|
returnError bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, projectId string) (*objectstorage.ProjectStatus, error) {
|
func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, projectId, _ string) (*objectstorage.ProjectStatus, error) {
|
||||||
if c.returnError {
|
if c.returnError {
|
||||||
return nil, fmt.Errorf("create project failed")
|
return nil, fmt.Errorf("create project failed")
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +51,7 @@ func TestMapFields(t *testing.T) {
|
||||||
AccessKey: types.StringNull(),
|
AccessKey: types.StringNull(),
|
||||||
SecretAccessKey: types.StringNull(),
|
SecretAccessKey: types.StringNull(),
|
||||||
ExpirationTimestamp: types.StringNull(),
|
ExpirationTimestamp: types.StringNull(),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
@ -71,6 +72,7 @@ func TestMapFields(t *testing.T) {
|
||||||
AccessKey: types.StringValue("key"),
|
AccessKey: types.StringValue("key"),
|
||||||
SecretAccessKey: types.StringValue("secret-key"),
|
SecretAccessKey: types.StringValue("secret-key"),
|
||||||
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
@ -90,6 +92,7 @@ func TestMapFields(t *testing.T) {
|
||||||
AccessKey: types.StringValue(""),
|
AccessKey: types.StringValue(""),
|
||||||
SecretAccessKey: types.StringValue(""),
|
SecretAccessKey: types.StringValue(""),
|
||||||
ExpirationTimestamp: types.StringNull(),
|
ExpirationTimestamp: types.StringNull(),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
@ -106,6 +109,7 @@ func TestMapFields(t *testing.T) {
|
||||||
Name: types.StringNull(),
|
Name: types.StringNull(),
|
||||||
AccessKey: types.StringNull(),
|
AccessKey: types.StringNull(),
|
||||||
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
@ -131,7 +135,7 @@ func TestMapFields(t *testing.T) {
|
||||||
CredentialsGroupId: tt.expected.CredentialsGroupId,
|
CredentialsGroupId: tt.expected.CredentialsGroupId,
|
||||||
CredentialId: tt.expected.CredentialId,
|
CredentialId: tt.expected.CredentialId,
|
||||||
}
|
}
|
||||||
err := mapFields(tt.input, model)
|
err := mapFields(tt.input, model, "eu01")
|
||||||
if !tt.isValid && err == nil {
|
if !tt.isValid && err == nil {
|
||||||
t.Fatalf("Should have failed")
|
t.Fatalf("Should have failed")
|
||||||
}
|
}
|
||||||
|
|
@ -196,7 +200,7 @@ func TestEnableProject(t *testing.T) {
|
||||||
CredentialsGroupId: tt.expected.CredentialsGroupId,
|
CredentialsGroupId: tt.expected.CredentialsGroupId,
|
||||||
CredentialId: tt.expected.CredentialId,
|
CredentialId: tt.expected.CredentialId,
|
||||||
}
|
}
|
||||||
err := enableProject(context.Background(), model, client)
|
err := enableProject(context.Background(), model, "eu01", client)
|
||||||
if !tt.isValid && err == nil {
|
if !tt.isValid && err == nil {
|
||||||
t.Fatalf("Should have failed")
|
t.Fatalf("Should have failed")
|
||||||
}
|
}
|
||||||
|
|
@ -242,6 +246,7 @@ func TestReadCredentials(t *testing.T) {
|
||||||
AccessKey: types.StringNull(),
|
AccessKey: types.StringNull(),
|
||||||
SecretAccessKey: types.StringNull(),
|
SecretAccessKey: types.StringNull(),
|
||||||
ExpirationTimestamp: types.StringNull(),
|
ExpirationTimestamp: types.StringNull(),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
|
|
@ -277,6 +282,7 @@ func TestReadCredentials(t *testing.T) {
|
||||||
AccessKey: types.StringNull(),
|
AccessKey: types.StringNull(),
|
||||||
SecretAccessKey: types.StringNull(),
|
SecretAccessKey: types.StringNull(),
|
||||||
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
|
|
@ -312,6 +318,7 @@ func TestReadCredentials(t *testing.T) {
|
||||||
AccessKey: types.StringNull(),
|
AccessKey: types.StringNull(),
|
||||||
SecretAccessKey: types.StringNull(),
|
SecretAccessKey: types.StringNull(),
|
||||||
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
|
|
@ -322,7 +329,9 @@ func TestReadCredentials(t *testing.T) {
|
||||||
&objectstorage.ListAccessKeysResponse{
|
&objectstorage.ListAccessKeysResponse{
|
||||||
AccessKeys: &[]objectstorage.AccessKey{},
|
AccessKeys: &[]objectstorage.AccessKey{},
|
||||||
},
|
},
|
||||||
Model{},
|
Model{
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
|
},
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
|
@ -351,7 +360,9 @@ func TestReadCredentials(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Model{},
|
Model{
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
|
},
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
|
@ -413,7 +424,7 @@ func TestReadCredentials(t *testing.T) {
|
||||||
CredentialsGroupId: tt.expectedModel.CredentialsGroupId,
|
CredentialsGroupId: tt.expectedModel.CredentialsGroupId,
|
||||||
CredentialId: tt.expectedModel.CredentialId,
|
CredentialId: tt.expectedModel.CredentialId,
|
||||||
}
|
}
|
||||||
found, err := readCredentials(context.Background(), model, client)
|
found, err := readCredentials(context.Background(), model, "eu01", client)
|
||||||
if !tt.isValid && err == nil {
|
if !tt.isValid && err == nil {
|
||||||
t.Fatalf("Should have failed")
|
t.Fatalf("Should have failed")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||||
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||||
|
|
@ -27,7 +29,8 @@ func NewCredentialsGroupDataSource() datasource.DataSource {
|
||||||
|
|
||||||
// credentialsGroupDataSource is the data source implementation.
|
// credentialsGroupDataSource is the data source implementation.
|
||||||
type credentialsGroupDataSource struct {
|
type credentialsGroupDataSource struct {
|
||||||
client *objectstorage.APIClient
|
client *objectstorage.APIClient
|
||||||
|
providerData core.ProviderData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata returns the data source type name.
|
// Metadata returns the data source type name.
|
||||||
|
|
@ -42,7 +45,8 @@ func (r *credentialsGroupDataSource) Configure(ctx context.Context, req datasour
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
var ok bool
|
||||||
|
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||||
if !ok {
|
if !ok {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||||
return
|
return
|
||||||
|
|
@ -50,15 +54,14 @@ func (r *credentialsGroupDataSource) Configure(ctx context.Context, req datasour
|
||||||
|
|
||||||
var apiClient *objectstorage.APIClient
|
var apiClient *objectstorage.APIClient
|
||||||
var err error
|
var err error
|
||||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithRegion(providerData.Region),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,6 +83,7 @@ func (r *credentialsGroupDataSource) Schema(_ context.Context, _ datasource.Sche
|
||||||
"name": "The credentials group's display name.",
|
"name": "The credentials group's display name.",
|
||||||
"project_id": "Object Storage Project ID to which the credentials group is associated.",
|
"project_id": "Object Storage Project ID to which the credentials group is associated.",
|
||||||
"urn": "Credentials group uniform resource name (URN)",
|
"urn": "Credentials group uniform resource name (URN)",
|
||||||
|
"region": "The resource region. If not defined, the provider region is used.",
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
|
|
@ -111,6 +115,11 @@ func (r *credentialsGroupDataSource) Schema(_ context.Context, _ datasource.Sche
|
||||||
Computed: true,
|
Computed: true,
|
||||||
Description: descriptions["urn"],
|
Description: descriptions["urn"],
|
||||||
},
|
},
|
||||||
|
"region": schema.StringAttribute{
|
||||||
|
// the region cannot be found automatically, so it has to be passed
|
||||||
|
Optional: true,
|
||||||
|
Description: descriptions["region"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,10 +134,18 @@ func (r *credentialsGroupDataSource) Read(ctx context.Context, req datasource.Re
|
||||||
}
|
}
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||||
|
var region string
|
||||||
|
if utils.IsUndefined(model.Region) {
|
||||||
|
region = r.providerData.Region
|
||||||
|
} else {
|
||||||
|
region = model.Region.ValueString()
|
||||||
|
}
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
found, err := readCredentialsGroups(ctx, &model, r.client)
|
found, err := readCredentialsGroups(ctx, &model, region, r.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials group", fmt.Sprintf("getting credential group from list of credentials groups: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials group", fmt.Sprintf("getting credential group from list of credentials groups: %v", err))
|
||||||
return
|
return
|
||||||
|
|
@ -139,6 +156,10 @@ func (r *credentialsGroupDataSource) Read(ctx context.Context, req datasource.Re
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update the region attribute manually, as it is not contained in the
|
||||||
|
// server response
|
||||||
|
model.Region = types.StringValue(region)
|
||||||
|
|
||||||
// Set refreshed state
|
// Set refreshed state
|
||||||
diags = resp.State.Set(ctx, model)
|
diags = resp.State.Set(ctx, model)
|
||||||
resp.Diagnostics.Append(diags...)
|
resp.Diagnostics.Append(diags...)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||||
|
coreutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
|
@ -28,6 +29,7 @@ var (
|
||||||
_ resource.Resource = &credentialsGroupResource{}
|
_ resource.Resource = &credentialsGroupResource{}
|
||||||
_ resource.ResourceWithConfigure = &credentialsGroupResource{}
|
_ resource.ResourceWithConfigure = &credentialsGroupResource{}
|
||||||
_ resource.ResourceWithImportState = &credentialsGroupResource{}
|
_ resource.ResourceWithImportState = &credentialsGroupResource{}
|
||||||
|
_ resource.ResourceWithModifyPlan = &credentialsGroupResource{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
|
|
@ -36,6 +38,7 @@ type Model struct {
|
||||||
Name types.String `tfsdk:"name"`
|
Name types.String `tfsdk:"name"`
|
||||||
ProjectId types.String `tfsdk:"project_id"`
|
ProjectId types.String `tfsdk:"project_id"`
|
||||||
URN types.String `tfsdk:"urn"`
|
URN types.String `tfsdk:"urn"`
|
||||||
|
Region types.String `tfsdk:"region"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCredentialsGroupResource is a helper function to simplify the provider implementation.
|
// NewCredentialsGroupResource is a helper function to simplify the provider implementation.
|
||||||
|
|
@ -45,7 +48,38 @@ func NewCredentialsGroupResource() resource.Resource {
|
||||||
|
|
||||||
// credentialsGroupResource is the resource implementation.
|
// credentialsGroupResource is the resource implementation.
|
||||||
type credentialsGroupResource struct {
|
type credentialsGroupResource struct {
|
||||||
client *objectstorage.APIClient
|
client *objectstorage.APIClient
|
||||||
|
providerData core.ProviderData
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModifyPlan implements resource.ResourceWithModifyPlan.
|
||||||
|
// Use the modifier to set the effective region in the current plan.
|
||||||
|
func (r *credentialsGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
|
||||||
|
var configModel Model
|
||||||
|
// skip initial empty configuration to avoid follow-up errors
|
||||||
|
if req.Config.Raw.IsNull() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var planModel Model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
coreutils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.Region, resp)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metadata returns the resource type name.
|
// Metadata returns the resource type name.
|
||||||
|
|
@ -60,7 +94,8 @@ func (r *credentialsGroupResource) Configure(ctx context.Context, req resource.C
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
var ok bool
|
||||||
|
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||||
if !ok {
|
if !ok {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||||
return
|
return
|
||||||
|
|
@ -68,15 +103,14 @@ func (r *credentialsGroupResource) Configure(ctx context.Context, req resource.C
|
||||||
|
|
||||||
var apiClient *objectstorage.APIClient
|
var apiClient *objectstorage.APIClient
|
||||||
var err error
|
var err error
|
||||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
apiClient, err = objectstorage.NewAPIClient(
|
apiClient, err = objectstorage.NewAPIClient(
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||||
config.WithRegion(providerData.Region),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,6 +132,7 @@ func (r *credentialsGroupResource) Schema(_ context.Context, _ resource.SchemaRe
|
||||||
"name": "The credentials group's display name.",
|
"name": "The credentials group's display name.",
|
||||||
"project_id": "Project ID to which the credentials group is associated.",
|
"project_id": "Project ID to which the credentials group is associated.",
|
||||||
"urn": "Credentials group uniform resource name (URN)",
|
"urn": "Credentials group uniform resource name (URN)",
|
||||||
|
"region": "The resource region. If not defined, the provider region is used.",
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
|
|
@ -138,6 +173,15 @@ func (r *credentialsGroupResource) Schema(_ context.Context, _ resource.SchemaRe
|
||||||
Description: descriptions["urn"],
|
Description: descriptions["urn"],
|
||||||
Computed: true,
|
Computed: true,
|
||||||
},
|
},
|
||||||
|
"region": schema.StringAttribute{
|
||||||
|
Optional: true,
|
||||||
|
// must be computed to allow for storing the override value from the provider
|
||||||
|
Computed: true,
|
||||||
|
Description: descriptions["region"],
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -152,29 +196,32 @@ func (r *credentialsGroupResource) Create(ctx context.Context, req resource.Crea
|
||||||
}
|
}
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
credentialsGroupName := model.Name.ValueString()
|
credentialsGroupName := model.Name.ValueString()
|
||||||
|
region := model.Region.ValueString()
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "name", credentialsGroupName)
|
ctx = tflog.SetField(ctx, "name", credentialsGroupName)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
createCredentialsGroupPayload := objectstorage.CreateCredentialsGroupPayload{
|
createCredentialsGroupPayload := objectstorage.CreateCredentialsGroupPayload{
|
||||||
DisplayName: utils.Ptr(credentialsGroupName),
|
DisplayName: utils.Ptr(credentialsGroupName),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle project init
|
// Handle project init
|
||||||
err := enableProject(ctx, &model, r.client)
|
err := enableProject(ctx, &model, region, r.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials group", fmt.Sprintf("Enabling object storage project before creation: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials group", fmt.Sprintf("Enabling object storage project before creation: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new credentials group
|
// Create new credentials group
|
||||||
got, err := r.client.CreateCredentialsGroup(ctx, projectId).CreateCredentialsGroupPayload(createCredentialsGroupPayload).Execute()
|
got, err := r.client.CreateCredentialsGroup(ctx, projectId, region).CreateCredentialsGroupPayload(createCredentialsGroupPayload).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials group", fmt.Sprintf("Calling API: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials group", fmt.Sprintf("Calling API: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map response body to schema
|
// Map response body to schema
|
||||||
err = mapFields(got, &model)
|
err = mapFields(got, &model, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentialsGroup", fmt.Sprintf("Processing API payload: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentialsGroup", fmt.Sprintf("Processing API payload: %v", err))
|
||||||
return
|
return
|
||||||
|
|
@ -197,10 +244,13 @@ func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRe
|
||||||
}
|
}
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||||
|
region := model.Region.ValueString()
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
found, err := readCredentialsGroups(ctx, &model, r.client)
|
found, err := readCredentialsGroups(ctx, &model, region, r.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentialsGroup", fmt.Sprintf("getting credential group from list of credentials groups: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentialsGroup", fmt.Sprintf("getting credential group from list of credentials groups: %v", err))
|
||||||
return
|
return
|
||||||
|
|
@ -209,6 +259,8 @@ func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRe
|
||||||
resp.State.RemoveResource(ctx)
|
resp.State.RemoveResource(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// update the region manually
|
||||||
|
model.Region = types.StringValue(region)
|
||||||
|
|
||||||
// Set refreshed state
|
// Set refreshed state
|
||||||
diags = resp.State.Set(ctx, model)
|
diags = resp.State.Set(ctx, model)
|
||||||
|
|
@ -235,11 +287,14 @@ func (r *credentialsGroupResource) Delete(ctx context.Context, req resource.Dele
|
||||||
}
|
}
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||||
|
region := model.Region.ValueString()
|
||||||
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||||
|
ctx = tflog.SetField(ctx, "region", region)
|
||||||
|
|
||||||
// Delete existing credentials group
|
// Delete existing credentials group
|
||||||
_, err := r.client.DeleteCredentialsGroup(ctx, projectId, credentialsGroupId).Execute()
|
_, err := r.client.DeleteCredentialsGroup(ctx, projectId, region, credentialsGroupId).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials group", fmt.Sprintf("Calling API: %v", err))
|
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials group", fmt.Sprintf("Calling API: %v", err))
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +319,7 @@ func (r *credentialsGroupResource) ImportState(ctx context.Context, req resource
|
||||||
tflog.Info(ctx, "ObjectStorage credentials group state imported")
|
tflog.Info(ctx, "ObjectStorage credentials group state imported")
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapFields(credentialsGroupResp *objectstorage.CreateCredentialsGroupResponse, model *Model) error {
|
func mapFields(credentialsGroupResp *objectstorage.CreateCredentialsGroupResponse, model *Model, region string) error {
|
||||||
if credentialsGroupResp == nil {
|
if credentialsGroupResp == nil {
|
||||||
return fmt.Errorf("response input is nil")
|
return fmt.Errorf("response input is nil")
|
||||||
}
|
}
|
||||||
|
|
@ -280,12 +335,13 @@ func mapFields(credentialsGroupResp *objectstorage.CreateCredentialsGroupRespons
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
model.Region = types.StringValue(region)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapCredentialsGroup(credentialsGroup objectstorage.CredentialsGroup, model *Model) error {
|
func mapCredentialsGroup(credentialsGroup objectstorage.CredentialsGroup, model *Model) error {
|
||||||
var credentialsGroupId string
|
var credentialsGroupId string
|
||||||
if model.CredentialsGroupId.ValueString() != "" {
|
if !coreutils.IsUndefined(model.CredentialsGroupId) {
|
||||||
credentialsGroupId = model.CredentialsGroupId.ValueString()
|
credentialsGroupId = model.CredentialsGroupId.ValueString()
|
||||||
} else if credentialsGroup.CredentialsGroupId != nil {
|
} else if credentialsGroup.CredentialsGroupId != nil {
|
||||||
credentialsGroupId = *credentialsGroup.CredentialsGroupId
|
credentialsGroupId = *credentialsGroup.CredentialsGroupId
|
||||||
|
|
@ -307,16 +363,16 @@ func mapCredentialsGroup(credentialsGroup objectstorage.CredentialsGroup, model
|
||||||
}
|
}
|
||||||
|
|
||||||
type objectStorageClient interface {
|
type objectStorageClient interface {
|
||||||
EnableServiceExecute(ctx context.Context, projectId string) (*objectstorage.ProjectStatus, error)
|
EnableServiceExecute(ctx context.Context, projectId, region string) (*objectstorage.ProjectStatus, error)
|
||||||
ListCredentialsGroupsExecute(ctx context.Context, projectId string) (*objectstorage.ListCredentialsGroupsResponse, error)
|
ListCredentialsGroupsExecute(ctx context.Context, projectId, region string) (*objectstorage.ListCredentialsGroupsResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens
|
// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens
|
||||||
func enableProject(ctx context.Context, model *Model, client objectStorageClient) error {
|
func enableProject(ctx context.Context, model *Model, region string, client objectStorageClient) error {
|
||||||
projectId := model.ProjectId.ValueString()
|
projectId := model.ProjectId.ValueString()
|
||||||
|
|
||||||
// From the object storage OAS: Creation will also be successful if the project is already enabled, but will not create a duplicate
|
// From the object storage OAS: Creation will also be successful if the project is already enabled, but will not create a duplicate
|
||||||
_, err := client.EnableServiceExecute(ctx, projectId)
|
_, err := client.EnableServiceExecute(ctx, projectId, region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create object storage project: %w", err)
|
return fmt.Errorf("failed to create object storage project: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -326,14 +382,14 @@ func enableProject(ctx context.Context, model *Model, client objectStorageClient
|
||||||
// readCredentialsGroups gets all the existing credentials groups for the specified project,
|
// readCredentialsGroups gets all the existing credentials groups for the specified project,
|
||||||
// finds the credentials group that is being read and updates the state.
|
// finds the credentials group that is being read and updates the state.
|
||||||
// Returns True if the credential was found, False otherwise.
|
// Returns True if the credential was found, False otherwise.
|
||||||
func readCredentialsGroups(ctx context.Context, model *Model, client objectStorageClient) (bool, error) {
|
func readCredentialsGroups(ctx context.Context, model *Model, region string, client objectStorageClient) (bool, error) {
|
||||||
found := false
|
found := false
|
||||||
|
|
||||||
if model.CredentialsGroupId.ValueString() == "" && model.Name.ValueString() == "" {
|
if model.CredentialsGroupId.ValueString() == "" && model.Name.ValueString() == "" {
|
||||||
return found, fmt.Errorf("missing configuration: either name or credentials group id must be provided")
|
return found, fmt.Errorf("missing configuration: either name or credentials group id must be provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
credentialsGroupsResp, err := client.ListCredentialsGroupsExecute(ctx, model.ProjectId.ValueString())
|
credentialsGroupsResp, err := client.ListCredentialsGroupsExecute(ctx, model.ProjectId.ValueString(), region)
|
||||||
if err != nil {
|
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
|
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.StatusNotFound {
|
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ type objectStorageClientMocked struct {
|
||||||
listCredentialsGroupsResp *objectstorage.ListCredentialsGroupsResponse
|
listCredentialsGroupsResp *objectstorage.ListCredentialsGroupsResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, projectId string) (*objectstorage.ProjectStatus, error) {
|
func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, projectId, _ string) (*objectstorage.ProjectStatus, error) {
|
||||||
if c.returnError {
|
if c.returnError {
|
||||||
return nil, fmt.Errorf("create project failed")
|
return nil, fmt.Errorf("create project failed")
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, proj
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *objectStorageClientMocked) ListCredentialsGroupsExecute(_ context.Context, _ string) (*objectstorage.ListCredentialsGroupsResponse, error) {
|
func (c *objectStorageClientMocked) ListCredentialsGroupsExecute(_ context.Context, _, _ string) (*objectstorage.ListCredentialsGroupsResponse, error) {
|
||||||
if c.returnError {
|
if c.returnError {
|
||||||
return nil, fmt.Errorf("get credentials groups failed")
|
return nil, fmt.Errorf("get credentials groups failed")
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +52,7 @@ func TestMapFields(t *testing.T) {
|
||||||
ProjectId: types.StringValue("pid"),
|
ProjectId: types.StringValue("pid"),
|
||||||
CredentialsGroupId: types.StringValue("cid"),
|
CredentialsGroupId: types.StringValue("cid"),
|
||||||
URN: types.StringNull(),
|
URN: types.StringNull(),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
@ -69,6 +70,7 @@ func TestMapFields(t *testing.T) {
|
||||||
ProjectId: types.StringValue("pid"),
|
ProjectId: types.StringValue("pid"),
|
||||||
CredentialsGroupId: types.StringValue("cid"),
|
CredentialsGroupId: types.StringValue("cid"),
|
||||||
URN: types.StringValue("urn"),
|
URN: types.StringValue("urn"),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
@ -86,6 +88,7 @@ func TestMapFields(t *testing.T) {
|
||||||
ProjectId: types.StringValue("pid"),
|
ProjectId: types.StringValue("pid"),
|
||||||
CredentialsGroupId: types.StringValue("cid"),
|
CredentialsGroupId: types.StringValue("cid"),
|
||||||
URN: types.StringValue(""),
|
URN: types.StringValue(""),
|
||||||
|
Region: types.StringValue("eu01"),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
@ -108,7 +111,7 @@ func TestMapFields(t *testing.T) {
|
||||||
ProjectId: tt.expected.ProjectId,
|
ProjectId: tt.expected.ProjectId,
|
||||||
CredentialsGroupId: tt.expected.CredentialsGroupId,
|
CredentialsGroupId: tt.expected.CredentialsGroupId,
|
||||||
}
|
}
|
||||||
err := mapFields(tt.input, model)
|
err := mapFields(tt.input, model, "eu01")
|
||||||
if !tt.isValid && err == nil {
|
if !tt.isValid && err == nil {
|
||||||
t.Fatalf("Should have failed")
|
t.Fatalf("Should have failed")
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +150,7 @@ func TestEnableProject(t *testing.T) {
|
||||||
client := &objectStorageClientMocked{
|
client := &objectStorageClientMocked{
|
||||||
returnError: tt.enableFails,
|
returnError: tt.enableFails,
|
||||||
}
|
}
|
||||||
err := enableProject(context.Background(), &Model{}, client)
|
err := enableProject(context.Background(), &Model{}, "eu01", client)
|
||||||
if !tt.isValid && err == nil {
|
if !tt.isValid && err == nil {
|
||||||
t.Fatalf("Should have failed")
|
t.Fatalf("Should have failed")
|
||||||
}
|
}
|
||||||
|
|
@ -288,7 +291,7 @@ func TestReadCredentialsGroups(t *testing.T) {
|
||||||
ProjectId: tt.expectedModel.ProjectId,
|
ProjectId: tt.expectedModel.ProjectId,
|
||||||
CredentialsGroupId: tt.expectedModel.CredentialsGroupId,
|
CredentialsGroupId: tt.expectedModel.CredentialsGroupId,
|
||||||
}
|
}
|
||||||
found, err := readCredentialsGroups(context.Background(), model, client)
|
found, err := readCredentialsGroups(context.Background(), model, "eu01", client)
|
||||||
if !tt.isValid && err == nil {
|
if !tt.isValid && err == nil {
|
||||||
t.Fatalf("Should have failed")
|
t.Fatalf("Should have failed")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,7 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error {
|
||||||
bucketsToDestroy = append(bucketsToDestroy, bucketName)
|
bucketsToDestroy = append(bucketsToDestroy, bucketName)
|
||||||
}
|
}
|
||||||
|
|
||||||
bucketsResp, err := client.ListBuckets(ctx, testutil.ProjectId).Execute()
|
bucketsResp, err := client.ListBuckets(ctx, testutil.ProjectId, testutil.Region).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getting bucketsResp: %w", err)
|
return fmt.Errorf("getting bucketsResp: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -260,11 +260,11 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error {
|
||||||
}
|
}
|
||||||
bucketName := *bucket.Name
|
bucketName := *bucket.Name
|
||||||
if utils.Contains(bucketsToDestroy, bucketName) {
|
if utils.Contains(bucketsToDestroy, bucketName) {
|
||||||
_, err := client.DeleteBucketExecute(ctx, testutil.ProjectId, bucketName)
|
_, err := client.DeleteBucketExecute(ctx, testutil.ProjectId, testutil.Region, bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("destroying bucket %s during CheckDestroy: %w", bucketName, err)
|
return fmt.Errorf("destroying bucket %s during CheckDestroy: %w", bucketName, err)
|
||||||
}
|
}
|
||||||
_, err = wait.DeleteBucketWaitHandler(ctx, client, testutil.ProjectId, bucketName).WaitWithContext(ctx)
|
_, err = wait.DeleteBucketWaitHandler(ctx, client, testutil.ProjectId, testutil.Region, bucketName).WaitWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", bucketName, err)
|
return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", bucketName, err)
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +281,7 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error {
|
||||||
credentialsGroupsToDestroy = append(credentialsGroupsToDestroy, credentialsGroupId)
|
credentialsGroupsToDestroy = append(credentialsGroupsToDestroy, credentialsGroupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
credentialsGroupsResp, err := client.ListCredentialsGroups(ctx, testutil.ProjectId).Execute()
|
credentialsGroupsResp, err := client.ListCredentialsGroups(ctx, testutil.ProjectId, testutil.Region).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getting bucketsResp: %w", err)
|
return fmt.Errorf("getting bucketsResp: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +293,7 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error {
|
||||||
}
|
}
|
||||||
groupId := *group.CredentialsGroupId
|
groupId := *group.CredentialsGroupId
|
||||||
if utils.Contains(credentialsGroupsToDestroy, groupId) {
|
if utils.Contains(credentialsGroupsToDestroy, groupId) {
|
||||||
_, err := client.DeleteCredentialsGroupExecute(ctx, testutil.ProjectId, groupId)
|
_, err := client.DeleteCredentialsGroupExecute(ctx, testutil.ProjectId, testutil.Region, groupId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("destroying credentials group %s during CheckDestroy: %w", groupId, err)
|
return fmt.Errorf("destroying credentials group %s during CheckDestroy: %w", groupId, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ var (
|
||||||
OrganizationId = os.Getenv("TF_ACC_ORGANIZATION_ID")
|
OrganizationId = os.Getenv("TF_ACC_ORGANIZATION_ID")
|
||||||
// ProjectId is the id of project used for tests
|
// ProjectId is the id of project used for tests
|
||||||
ProjectId = os.Getenv("TF_ACC_PROJECT_ID")
|
ProjectId = os.Getenv("TF_ACC_PROJECT_ID")
|
||||||
|
Region = os.Getenv("TF_ACC_REGION")
|
||||||
// ServerId is the id of a server used for some tests
|
// ServerId is the id of a server used for some tests
|
||||||
ServerId = getenv("TF_ACC_SERVER_ID", "")
|
ServerId = getenv("TF_ACC_SERVER_ID", "")
|
||||||
// IaaSImageId is the id of an image used for IaaS acceptance tests. Once the stackit_image resource is implemented, we can remove this
|
// IaaSImageId is the id of an image used for IaaS acceptance tests. Once the stackit_image resource is implemented, we can remove this
|
||||||
|
|
|
||||||
39
stackit/internal/utils/regions.go
Normal file
39
stackit/internal/utils/regions.go
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdaptRegion rewrites the region of a terraform plan
|
||||||
|
func AdaptRegion(ctx context.Context, configRegion types.String, planRegion *types.String, defaultRegion string, resp *resource.ModifyPlanResponse) {
|
||||||
|
// Get the intended region. This is either set directly set in the individual
|
||||||
|
// config or the provider region has to be used
|
||||||
|
var intendedRegion types.String
|
||||||
|
if configRegion.IsNull() {
|
||||||
|
if defaultRegion == "" {
|
||||||
|
core.LogAndAddError(ctx, &resp.Diagnostics, "set region", "no region defined in config or provider")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
intendedRegion = types.StringValue(defaultRegion)
|
||||||
|
} else {
|
||||||
|
intendedRegion = configRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the currently configured region corresponds to the planned region
|
||||||
|
// on mismatch override the planned region with the intended region
|
||||||
|
// and force a replace of the resource
|
||||||
|
p := path.Root("region")
|
||||||
|
if !intendedRegion.Equal(*planRegion) {
|
||||||
|
resp.RequiresReplace.Append(p)
|
||||||
|
*planRegion = intendedRegion
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, p, *planRegion)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
87
stackit/internal/utils/regions_test.go
Normal file
87
stackit/internal/utils/regions_test.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAdaptRegion(t *testing.T) {
|
||||||
|
type model struct {
|
||||||
|
Region types.String `tfsdk:"region"`
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
configRegion types.String
|
||||||
|
defaultRegion string
|
||||||
|
}
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
wantRegion types.String
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"no configured region, use provider region",
|
||||||
|
args{
|
||||||
|
types.StringNull(),
|
||||||
|
"eu01",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
types.StringValue("eu01"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no configured region, no provider region => want error",
|
||||||
|
args{
|
||||||
|
types.StringNull(),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
types.StringNull(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"configuration region overrides provider region",
|
||||||
|
args{
|
||||||
|
types.StringValue("eu01-m"),
|
||||||
|
"eu01",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
types.StringValue("eu01-m"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
plan := tfsdk.Plan{
|
||||||
|
Schema: schema.Schema{
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"region": schema.StringAttribute{
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if diags := plan.Set(context.Background(), model{types.StringValue("unknown")}); diags.HasError() {
|
||||||
|
t.Fatalf("cannot create test model: %v", diags)
|
||||||
|
}
|
||||||
|
resp := resource.ModifyPlanResponse{
|
||||||
|
Plan: plan,
|
||||||
|
}
|
||||||
|
|
||||||
|
configModel := model{
|
||||||
|
Region: tc.args.configRegion,
|
||||||
|
}
|
||||||
|
planModel := model{}
|
||||||
|
AdaptRegion(context.Background(), configModel.Region, &planModel.Region, tc.args.defaultRegion, &resp)
|
||||||
|
if diags := resp.Diagnostics; tc.wantErr != diags.HasError() {
|
||||||
|
t.Errorf("unexpected diagnostics: want err: %v, actual %v", tc.wantErr, diags.Errors())
|
||||||
|
}
|
||||||
|
if expected, actual := tc.wantRegion, planModel.Region; !expected.Equal(actual) {
|
||||||
|
t.Errorf("wrong result region. expect %s but got %s", expected, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -104,3 +104,13 @@ func QuoteValues(values []string) []string {
|
||||||
func IsLegacyProjectRole(role string) bool {
|
func IsLegacyProjectRole(role string) bool {
|
||||||
return utils.Contains(LegacyProjectRoles, role)
|
return utils.Contains(LegacyProjectRoles, role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type value interface {
|
||||||
|
IsUnknown() bool
|
||||||
|
IsNull() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUndefined checks if a passed value is unknown or null
|
||||||
|
func IsUndefined(val value) bool {
|
||||||
|
return val.IsUnknown() || val.IsNull()
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue