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.
|
||||
- `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
|
||||
|
||||
- `id` (String) Terraform's internal data source identifier. It is structured as "`project_id`,`name`".
|
||||
- `url_path_style` (String)
|
||||
- `url_virtual_hosted_style` (String)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ data "stackit_objectstorage_credentials_group" "example" {
|
|||
- `credential_id` (String) The credential ID.
|
||||
- `credentials_group_id` (String) The credential group ID.
|
||||
- `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
|
||||
|
||||
|
|
@ -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`".
|
||||
- `name` (String)
|
||||
- `secret_access_key` (String, Sensitive)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ data "stackit_objectstorage_credentials_group" "example" {
|
|||
### Required
|
||||
|
||||
- `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
|
||||
|
||||
|
|
@ -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`".
|
||||
- `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.
|
||||
- `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
|
||||
|
||||
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`name`".
|
||||
- `url_path_style` (String)
|
||||
- `url_virtual_hosted_style` (String)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ resource "stackit_objectstorage_credential" "example" {
|
|||
### 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.
|
||||
- `region` (String) The resource region. If not defined, the provider region is used.
|
||||
|
||||
### 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`".
|
||||
- `name` (String)
|
||||
- `secret_access_key` (String, Sensitive)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,14 @@ resource "stackit_objectstorage_credentials_group" "example" {
|
|||
- `name` (String) The credentials group's display name.
|
||||
- `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
|
||||
|
||||
- `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`".
|
||||
- `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/mariadb v0.20.1
|
||||
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/opensearch v0.19.1
|
||||
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/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.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/go.mod h1:nVllQfYODhX1q3bgwVTLO7wHOp+8NMLiKbn3u/Dg5nU=
|
||||
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/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/objectstorage v0.11.1 h1:Df3fTAHaVgyiiyp9LyTTQI8jXSVeGo49eW5ya4AATCY=
|
||||
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 h1:/0n2zcH1nMw2noroGhz0fgu2YqtNo9v3AsVhXMRtmtw=
|
||||
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/go.mod h1:okcRTrNDTI3d7MQcYJMliK0qoXeLq0b1wvZuEqgJIWE=
|
||||
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-log/tflog"
|
||||
"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/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
|
|
@ -29,7 +30,8 @@ func NewBucketDataSource() datasource.DataSource {
|
|||
|
||||
// bucketDataSource is the data source implementation.
|
||||
type bucketDataSource struct {
|
||||
client *objectstorage.APIClient
|
||||
client *objectstorage.APIClient
|
||||
providerData core.ProviderData
|
||||
}
|
||||
|
||||
// Metadata returns the data source type name.
|
||||
|
|
@ -44,7 +46,8 @@ func (r *bucketDataSource) Configure(ctx context.Context, req datasource.Configu
|
|||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
var ok bool
|
||||
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
|
|
@ -52,15 +55,14 @@ func (r *bucketDataSource) Configure(ctx context.Context, req datasource.Configu
|
|||
|
||||
var apiClient *objectstorage.APIClient
|
||||
var err error
|
||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
||||
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +84,7 @@ func (r *bucketDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
|
|||
"project_id": "STACKIT Project ID to which the bucket is associated.",
|
||||
"url_path_style": "URL in path 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{
|
||||
|
|
@ -112,6 +115,11 @@ func (r *bucketDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
|
|||
"url_virtual_hosted_style": schema.StringAttribute{
|
||||
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()
|
||||
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, "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 {
|
||||
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 {
|
||||
|
|
@ -140,7 +156,7 @@ func (r *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest,
|
|||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(bucketResp, &model)
|
||||
err = mapFields(bucketResp, &model, region)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"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/hashicorp/terraform-plugin-framework/path"
|
||||
|
|
@ -28,6 +29,7 @@ var (
|
|||
_ resource.Resource = &bucketResource{}
|
||||
_ resource.ResourceWithConfigure = &bucketResource{}
|
||||
_ resource.ResourceWithImportState = &bucketResource{}
|
||||
_ resource.ResourceWithModifyPlan = &bucketResource{}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
|
@ -36,6 +38,7 @@ type Model struct {
|
|||
ProjectId types.String `tfsdk:"project_id"`
|
||||
URLPathStyle types.String `tfsdk:"url_path_style"`
|
||||
URLVirtualHostedStyle types.String `tfsdk:"url_virtual_hosted_style"`
|
||||
Region types.String `tfsdk:"region"`
|
||||
}
|
||||
|
||||
// NewBucketResource is a helper function to simplify the provider implementation.
|
||||
|
|
@ -45,7 +48,38 @@ func NewBucketResource() resource.Resource {
|
|||
|
||||
// bucketResource is the resource implementation.
|
||||
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.
|
||||
|
|
@ -60,7 +94,8 @@ func (r *bucketResource) Configure(ctx context.Context, req resource.ConfigureRe
|
|||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
var ok bool
|
||||
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
|
|
@ -68,15 +103,14 @@ func (r *bucketResource) Configure(ctx context.Context, req resource.ConfigureRe
|
|||
|
||||
var apiClient *objectstorage.APIClient
|
||||
var err error
|
||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
||||
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +132,7 @@ func (r *bucketResource) Schema(_ context.Context, _ resource.SchemaRequest, res
|
|||
"project_id": "STACKIT Project ID to which the bucket is associated.",
|
||||
"url_path_style": "URL in path 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{
|
||||
|
|
@ -139,6 +174,15 @@ func (r *bucketResource) Schema(_ context.Context, _ resource.SchemaRequest, res
|
|||
"url_virtual_hosted_style": schema.StringAttribute{
|
||||
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()
|
||||
bucketName := model.Name.ValueString()
|
||||
region := model.Region.ValueString()
|
||||
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "name", bucketName)
|
||||
ctx = tflog.SetField(ctx, "region", region)
|
||||
|
||||
// Handle project init
|
||||
err := enableProject(ctx, &model, r.client)
|
||||
err := enableProject(ctx, &model, region, r.client)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Enabling object storage project before creation: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Create new bucket
|
||||
_, err = r.client.CreateBucket(ctx, projectId, bucketName).Execute()
|
||||
_, err = r.client.CreateBucket(ctx, projectId, region, bucketName).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Calling API: %v", err))
|
||||
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 {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Bucket creation waiting: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(waitResp, &model)
|
||||
err = mapFields(waitResp, &model, region)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
|
|
@ -200,10 +247,13 @@ func (r *bucketResource) Read(ctx context.Context, req resource.ReadRequest, res
|
|||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
bucketName := model.Name.ValueString()
|
||||
region := model.Region.ValueString()
|
||||
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
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 {
|
||||
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 {
|
||||
|
|
@ -215,7 +265,7 @@ func (r *bucketResource) Read(ctx context.Context, req resource.ReadRequest, res
|
|||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(bucketResp, &model)
|
||||
err = mapFields(bucketResp, &model, region)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
|
|
@ -246,15 +296,18 @@ func (r *bucketResource) Delete(ctx context.Context, req resource.DeleteRequest,
|
|||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
bucketName := model.Name.ValueString()
|
||||
region := model.Region.ValueString()
|
||||
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "name", bucketName)
|
||||
ctx = tflog.SetField(ctx, "region", region)
|
||||
|
||||
// Delete existing bucket
|
||||
_, err := r.client.DeleteBucket(ctx, projectId, bucketName).Execute()
|
||||
_, err := r.client.DeleteBucket(ctx, projectId, region, bucketName).Execute()
|
||||
if err != nil {
|
||||
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 {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", fmt.Sprintf("Bucket deletion waiting: %v", err))
|
||||
return
|
||||
|
|
@ -279,7 +332,7 @@ func (r *bucketResource) ImportState(ctx context.Context, req resource.ImportSta
|
|||
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 {
|
||||
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.URLVirtualHostedStyle = types.StringPointerValue(bucket.UrlVirtualHostedStyle)
|
||||
model.Region = types.StringValue(region)
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
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()
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("failed to create object storage project: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package objectstorage
|
|||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ type objectStorageClientMocked struct {
|
|||
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 {
|
||||
return nil, fmt.Errorf("create project failed")
|
||||
}
|
||||
|
|
@ -43,6 +44,7 @@ func TestMapFields(t *testing.T) {
|
|||
ProjectId: types.StringValue("pid"),
|
||||
URLPathStyle: types.StringNull(),
|
||||
URLVirtualHostedStyle: types.StringNull(),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -60,6 +62,7 @@ func TestMapFields(t *testing.T) {
|
|||
ProjectId: types.StringValue("pid"),
|
||||
URLPathStyle: types.StringValue("url/path/style"),
|
||||
URLVirtualHostedStyle: types.StringValue("url/virtual/hosted/style"),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -77,6 +80,7 @@ func TestMapFields(t *testing.T) {
|
|||
ProjectId: types.StringValue("pid"),
|
||||
URLPathStyle: types.StringValue(""),
|
||||
URLVirtualHostedStyle: types.StringValue(""),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -99,7 +103,7 @@ func TestMapFields(t *testing.T) {
|
|||
ProjectId: tt.expected.ProjectId,
|
||||
Name: tt.expected.Name,
|
||||
}
|
||||
err := mapFields(tt.input, model)
|
||||
err := mapFields(tt.input, model, "eu01")
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
@ -138,7 +142,7 @@ func TestEnableProject(t *testing.T) {
|
|||
client := &objectStorageClientMocked{
|
||||
returnError: tt.enableFails,
|
||||
}
|
||||
err := enableProject(context.Background(), &Model{}, client)
|
||||
err := enableProject(context.Background(), &Model{}, "eu01", client)
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"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/stackitcloud/stackit-sdk-go/core/config"
|
||||
|
|
@ -25,7 +26,8 @@ func NewCredentialDataSource() datasource.DataSource {
|
|||
|
||||
// credentialDataSource is the resource implementation.
|
||||
type credentialDataSource struct {
|
||||
client *objectstorage.APIClient
|
||||
client *objectstorage.APIClient
|
||||
providerData core.ProviderData
|
||||
}
|
||||
|
||||
// Metadata returns the resource type name.
|
||||
|
|
@ -40,7 +42,8 @@ func (r *credentialDataSource) Configure(ctx context.Context, req datasource.Con
|
|||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
var ok bool
|
||||
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
|
|
@ -48,15 +51,14 @@ func (r *credentialDataSource) Configure(ctx context.Context, req datasource.Con
|
|||
|
||||
var apiClient *objectstorage.APIClient
|
||||
var err error
|
||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
||||
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +79,7 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ
|
|||
"credential_id": "The credential ID.",
|
||||
"credentials_group_id": "The credential group ID.",
|
||||
"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{
|
||||
|
|
@ -111,6 +114,11 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ
|
|||
"expiration_timestamp": schema.StringAttribute{
|
||||
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()
|
||||
credentialsGroupId := model.CredentialsGroupId.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, "credentials_group_id", credentialsGroupId)
|
||||
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 {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"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/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
|
|
@ -29,6 +30,7 @@ var (
|
|||
_ resource.Resource = &credentialResource{}
|
||||
_ resource.ResourceWithConfigure = &credentialResource{}
|
||||
_ resource.ResourceWithImportState = &credentialResource{}
|
||||
_ resource.ResourceWithModifyPlan = &credentialResource{}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
|
@ -40,6 +42,7 @@ type Model struct {
|
|||
AccessKey types.String `tfsdk:"access_key"`
|
||||
SecretAccessKey types.String `tfsdk:"secret_access_key"`
|
||||
ExpirationTimestamp types.String `tfsdk:"expiration_timestamp"`
|
||||
Region types.String `tfsdk:"region"`
|
||||
}
|
||||
|
||||
// NewCredentialResource is a helper function to simplify the provider implementation.
|
||||
|
|
@ -49,7 +52,38 @@ func NewCredentialResource() resource.Resource {
|
|||
|
||||
// credentialResource is the resource implementation.
|
||||
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.
|
||||
|
|
@ -64,7 +98,8 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu
|
|||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
var ok bool
|
||||
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
|
|
@ -72,15 +107,14 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu
|
|||
|
||||
var apiClient *objectstorage.APIClient
|
||||
var err error
|
||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
||||
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +136,7 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest,
|
|||
"credentials_group_id": "The credential group ID.",
|
||||
"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.",
|
||||
"region": "The resource region. If not defined, the provider region is used.",
|
||||
}
|
||||
|
||||
resp.Schema = schema.Schema{
|
||||
|
|
@ -169,6 +204,15 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest,
|
|||
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()
|
||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||
region := model.Region.ValueString()
|
||||
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||
ctx = tflog.SetField(ctx, "region", region)
|
||||
|
||||
// Handle project init
|
||||
err := enableProject(ctx, &model, r.client)
|
||||
err := enableProject(ctx, &model, region, r.client)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Enabling object storage project before creation: %v", err))
|
||||
return
|
||||
|
|
@ -200,7 +247,7 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ
|
|||
return
|
||||
}
|
||||
// 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 {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
|
|
@ -213,7 +260,7 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ
|
|||
ctx = tflog.SetField(ctx, "credential_id", credentialId)
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(credentialResp, &model)
|
||||
err = mapFields(credentialResp, &model, region)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
|
|
@ -238,11 +285,14 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest,
|
|||
projectId := model.ProjectId.ValueString()
|
||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||
credentialId := model.CredentialId.ValueString()
|
||||
region := model.Region.ValueString()
|
||||
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||
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 {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err))
|
||||
return
|
||||
|
|
@ -279,12 +329,15 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ
|
|||
projectId := model.ProjectId.ValueString()
|
||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||
credentialId := model.CredentialId.ValueString()
|
||||
region := model.Region.ValueString()
|
||||
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||
ctx = tflog.SetField(ctx, "credential_id", credentialId)
|
||||
ctx = tflog.SetField(ctx, "region", region)
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
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
|
||||
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()
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("failed to create object storage project: %w", err)
|
||||
}
|
||||
|
|
@ -348,7 +401,7 @@ func toCreatePayload(model *Model) (*objectstorage.CreateAccessKeyPayload, error
|
|||
}, nil
|
||||
}
|
||||
|
||||
func mapFields(credentialResp *objectstorage.CreateAccessKeyResponse, model *Model) error {
|
||||
func mapFields(credentialResp *objectstorage.CreateAccessKeyResponse, model *Model, region string) error {
|
||||
if credentialResp == 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.AccessKey = types.StringPointerValue(credentialResp.AccessKey)
|
||||
model.SecretAccessKey = types.StringPointerValue(credentialResp.SecretAccessKey)
|
||||
model.Region = types.StringValue(region)
|
||||
return nil
|
||||
}
|
||||
|
||||
// readCredentials gets all the existing credentials for the specified credentials group,
|
||||
// finds the credential that is being read and updates the state.
|
||||
// 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()
|
||||
credentialsGroupId := model.CredentialsGroupId.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 {
|
||||
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 {
|
||||
|
|
@ -443,6 +497,7 @@ func readCredentials(ctx context.Context, model *Model, client *objectstorage.AP
|
|||
}
|
||||
break
|
||||
}
|
||||
model.Region = types.StringValue(region)
|
||||
|
||||
return foundCredential, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ type objectStorageClientMocked struct {
|
|||
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 {
|
||||
return nil, fmt.Errorf("create project failed")
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ func TestMapFields(t *testing.T) {
|
|||
AccessKey: types.StringNull(),
|
||||
SecretAccessKey: types.StringNull(),
|
||||
ExpirationTimestamp: types.StringNull(),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -71,6 +72,7 @@ func TestMapFields(t *testing.T) {
|
|||
AccessKey: types.StringValue("key"),
|
||||
SecretAccessKey: types.StringValue("secret-key"),
|
||||
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -90,6 +92,7 @@ func TestMapFields(t *testing.T) {
|
|||
AccessKey: types.StringValue(""),
|
||||
SecretAccessKey: types.StringValue(""),
|
||||
ExpirationTimestamp: types.StringNull(),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -106,6 +109,7 @@ func TestMapFields(t *testing.T) {
|
|||
Name: types.StringNull(),
|
||||
AccessKey: types.StringNull(),
|
||||
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -131,7 +135,7 @@ func TestMapFields(t *testing.T) {
|
|||
CredentialsGroupId: tt.expected.CredentialsGroupId,
|
||||
CredentialId: tt.expected.CredentialId,
|
||||
}
|
||||
err := mapFields(tt.input, model)
|
||||
err := mapFields(tt.input, model, "eu01")
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
@ -196,7 +200,7 @@ func TestEnableProject(t *testing.T) {
|
|||
CredentialsGroupId: tt.expected.CredentialsGroupId,
|
||||
CredentialId: tt.expected.CredentialId,
|
||||
}
|
||||
err := enableProject(context.Background(), model, client)
|
||||
err := enableProject(context.Background(), model, "eu01", client)
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
@ -242,6 +246,7 @@ func TestReadCredentials(t *testing.T) {
|
|||
AccessKey: types.StringNull(),
|
||||
SecretAccessKey: types.StringNull(),
|
||||
ExpirationTimestamp: types.StringNull(),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
false,
|
||||
|
|
@ -277,6 +282,7 @@ func TestReadCredentials(t *testing.T) {
|
|||
AccessKey: types.StringNull(),
|
||||
SecretAccessKey: types.StringNull(),
|
||||
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
false,
|
||||
|
|
@ -312,6 +318,7 @@ func TestReadCredentials(t *testing.T) {
|
|||
AccessKey: types.StringNull(),
|
||||
SecretAccessKey: types.StringNull(),
|
||||
ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
false,
|
||||
|
|
@ -322,7 +329,9 @@ func TestReadCredentials(t *testing.T) {
|
|||
&objectstorage.ListAccessKeysResponse{
|
||||
AccessKeys: &[]objectstorage.AccessKey{},
|
||||
},
|
||||
Model{},
|
||||
Model{
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
|
|
@ -351,7 +360,9 @@ func TestReadCredentials(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Model{},
|
||||
Model{
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
|
|
@ -413,7 +424,7 @@ func TestReadCredentials(t *testing.T) {
|
|||
CredentialsGroupId: tt.expectedModel.CredentialsGroupId,
|
||||
CredentialId: tt.expectedModel.CredentialId,
|
||||
}
|
||||
found, err := readCredentials(context.Background(), model, client)
|
||||
found, err := readCredentials(context.Background(), model, "eu01", client)
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"github.com/stackitcloud/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/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
|
|
@ -27,7 +29,8 @@ func NewCredentialsGroupDataSource() datasource.DataSource {
|
|||
|
||||
// credentialsGroupDataSource is the data source implementation.
|
||||
type credentialsGroupDataSource struct {
|
||||
client *objectstorage.APIClient
|
||||
client *objectstorage.APIClient
|
||||
providerData core.ProviderData
|
||||
}
|
||||
|
||||
// Metadata returns the data source type name.
|
||||
|
|
@ -42,7 +45,8 @@ func (r *credentialsGroupDataSource) Configure(ctx context.Context, req datasour
|
|||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
var ok bool
|
||||
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
|
|
@ -50,15 +54,14 @@ func (r *credentialsGroupDataSource) Configure(ctx context.Context, req datasour
|
|||
|
||||
var apiClient *objectstorage.APIClient
|
||||
var err error
|
||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
||||
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +83,7 @@ func (r *credentialsGroupDataSource) Schema(_ context.Context, _ datasource.Sche
|
|||
"name": "The credentials group's display name.",
|
||||
"project_id": "Object Storage Project ID to which the credentials group is associated.",
|
||||
"urn": "Credentials group uniform resource name (URN)",
|
||||
"region": "The resource region. If not defined, the provider region is used.",
|
||||
}
|
||||
|
||||
resp.Schema = schema.Schema{
|
||||
|
|
@ -111,6 +115,11 @@ func (r *credentialsGroupDataSource) Schema(_ context.Context, _ datasource.Sche
|
|||
Computed: true,
|
||||
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()
|
||||
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, "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 {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials group", fmt.Sprintf("getting credential group from list of credentials groups: %v", err))
|
||||
return
|
||||
|
|
@ -139,6 +156,10 @@ func (r *credentialsGroupDataSource) Read(ctx context.Context, req datasource.Re
|
|||
return
|
||||
}
|
||||
|
||||
// update the region attribute manually, as it is not contained in the
|
||||
// server response
|
||||
model.Region = types.StringValue(region)
|
||||
|
||||
// Set refreshed state
|
||||
diags = resp.State.Set(ctx, model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"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/hashicorp/terraform-plugin-framework/path"
|
||||
|
|
@ -28,6 +29,7 @@ var (
|
|||
_ resource.Resource = &credentialsGroupResource{}
|
||||
_ resource.ResourceWithConfigure = &credentialsGroupResource{}
|
||||
_ resource.ResourceWithImportState = &credentialsGroupResource{}
|
||||
_ resource.ResourceWithModifyPlan = &credentialsGroupResource{}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
|
|
@ -36,6 +38,7 @@ type Model struct {
|
|||
Name types.String `tfsdk:"name"`
|
||||
ProjectId types.String `tfsdk:"project_id"`
|
||||
URN types.String `tfsdk:"urn"`
|
||||
Region types.String `tfsdk:"region"`
|
||||
}
|
||||
|
||||
// NewCredentialsGroupResource is a helper function to simplify the provider implementation.
|
||||
|
|
@ -45,7 +48,38 @@ func NewCredentialsGroupResource() resource.Resource {
|
|||
|
||||
// credentialsGroupResource is the resource implementation.
|
||||
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.
|
||||
|
|
@ -60,7 +94,8 @@ func (r *credentialsGroupResource) Configure(ctx context.Context, req resource.C
|
|||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
var ok bool
|
||||
r.providerData, ok = req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
|
|
@ -68,15 +103,14 @@ func (r *credentialsGroupResource) Configure(ctx context.Context, req resource.C
|
|||
|
||||
var apiClient *objectstorage.APIClient
|
||||
var err error
|
||||
if providerData.ObjectStorageCustomEndpoint != "" {
|
||||
if r.providerData.ObjectStorageCustomEndpoint != "" {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.ObjectStorageCustomEndpoint),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
config.WithEndpoint(r.providerData.ObjectStorageCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = objectstorage.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
config.WithCustomAuth(r.providerData.RoundTripper),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +132,7 @@ func (r *credentialsGroupResource) Schema(_ context.Context, _ resource.SchemaRe
|
|||
"name": "The credentials group's display name.",
|
||||
"project_id": "Project ID to which the credentials group is associated.",
|
||||
"urn": "Credentials group uniform resource name (URN)",
|
||||
"region": "The resource region. If not defined, the provider region is used.",
|
||||
}
|
||||
|
||||
resp.Schema = schema.Schema{
|
||||
|
|
@ -138,6 +173,15 @@ func (r *credentialsGroupResource) Schema(_ context.Context, _ resource.SchemaRe
|
|||
Description: descriptions["urn"],
|
||||
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()
|
||||
credentialsGroupName := model.Name.ValueString()
|
||||
region := model.Region.ValueString()
|
||||
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "name", credentialsGroupName)
|
||||
ctx = tflog.SetField(ctx, "region", region)
|
||||
|
||||
createCredentialsGroupPayload := objectstorage.CreateCredentialsGroupPayload{
|
||||
DisplayName: utils.Ptr(credentialsGroupName),
|
||||
}
|
||||
|
||||
// Handle project init
|
||||
err := enableProject(ctx, &model, r.client)
|
||||
err := enableProject(ctx, &model, region, r.client)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials group", fmt.Sprintf("Enabling object storage project before creation: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 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 {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials group", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(got, &model)
|
||||
err = mapFields(got, &model, region)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentialsGroup", fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
|
|
@ -197,10 +244,13 @@ func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRe
|
|||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||
region := model.Region.ValueString()
|
||||
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
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 {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentialsGroup", fmt.Sprintf("getting credential group from list of credentials groups: %v", err))
|
||||
return
|
||||
|
|
@ -209,6 +259,8 @@ func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRe
|
|||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
// update the region manually
|
||||
model.Region = types.StringValue(region)
|
||||
|
||||
// Set refreshed state
|
||||
diags = resp.State.Set(ctx, model)
|
||||
|
|
@ -235,11 +287,14 @@ func (r *credentialsGroupResource) Delete(ctx context.Context, req resource.Dele
|
|||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
credentialsGroupId := model.CredentialsGroupId.ValueString()
|
||||
region := model.Region.ValueString()
|
||||
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId)
|
||||
ctx = tflog.SetField(ctx, "region", region)
|
||||
|
||||
// Delete existing credentials group
|
||||
_, err := r.client.DeleteCredentialsGroup(ctx, projectId, credentialsGroupId).Execute()
|
||||
_, err := r.client.DeleteCredentialsGroup(ctx, projectId, region, credentialsGroupId).Execute()
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
func mapFields(credentialsGroupResp *objectstorage.CreateCredentialsGroupResponse, model *Model) error {
|
||||
func mapFields(credentialsGroupResp *objectstorage.CreateCredentialsGroupResponse, model *Model, region string) error {
|
||||
if credentialsGroupResp == nil {
|
||||
return fmt.Errorf("response input is nil")
|
||||
}
|
||||
|
|
@ -280,12 +335,13 @@ func mapFields(credentialsGroupResp *objectstorage.CreateCredentialsGroupRespons
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
model.Region = types.StringValue(region)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapCredentialsGroup(credentialsGroup objectstorage.CredentialsGroup, model *Model) error {
|
||||
var credentialsGroupId string
|
||||
if model.CredentialsGroupId.ValueString() != "" {
|
||||
if !coreutils.IsUndefined(model.CredentialsGroupId) {
|
||||
credentialsGroupId = model.CredentialsGroupId.ValueString()
|
||||
} else if credentialsGroup.CredentialsGroupId != nil {
|
||||
credentialsGroupId = *credentialsGroup.CredentialsGroupId
|
||||
|
|
@ -307,16 +363,16 @@ func mapCredentialsGroup(credentialsGroup objectstorage.CredentialsGroup, model
|
|||
}
|
||||
|
||||
type objectStorageClient interface {
|
||||
EnableServiceExecute(ctx context.Context, projectId string) (*objectstorage.ProjectStatus, error)
|
||||
ListCredentialsGroupsExecute(ctx context.Context, projectId string) (*objectstorage.ListCredentialsGroupsResponse, error)
|
||||
EnableServiceExecute(ctx context.Context, projectId, region string) (*objectstorage.ProjectStatus, 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
|
||||
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()
|
||||
|
||||
// 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 {
|
||||
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,
|
||||
// finds the credentials group that is being read and updates the state.
|
||||
// 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
|
||||
|
||||
if model.CredentialsGroupId.ValueString() == "" && model.Name.ValueString() == "" {
|
||||
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 {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ type objectStorageClientMocked struct {
|
|||
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 {
|
||||
return nil, fmt.Errorf("create project failed")
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, proj
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (c *objectStorageClientMocked) ListCredentialsGroupsExecute(_ context.Context, _ string) (*objectstorage.ListCredentialsGroupsResponse, error) {
|
||||
func (c *objectStorageClientMocked) ListCredentialsGroupsExecute(_ context.Context, _, _ string) (*objectstorage.ListCredentialsGroupsResponse, error) {
|
||||
if c.returnError {
|
||||
return nil, fmt.Errorf("get credentials groups failed")
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ func TestMapFields(t *testing.T) {
|
|||
ProjectId: types.StringValue("pid"),
|
||||
CredentialsGroupId: types.StringValue("cid"),
|
||||
URN: types.StringNull(),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -69,6 +70,7 @@ func TestMapFields(t *testing.T) {
|
|||
ProjectId: types.StringValue("pid"),
|
||||
CredentialsGroupId: types.StringValue("cid"),
|
||||
URN: types.StringValue("urn"),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -86,6 +88,7 @@ func TestMapFields(t *testing.T) {
|
|||
ProjectId: types.StringValue("pid"),
|
||||
CredentialsGroupId: types.StringValue("cid"),
|
||||
URN: types.StringValue(""),
|
||||
Region: types.StringValue("eu01"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -108,7 +111,7 @@ func TestMapFields(t *testing.T) {
|
|||
ProjectId: tt.expected.ProjectId,
|
||||
CredentialsGroupId: tt.expected.CredentialsGroupId,
|
||||
}
|
||||
err := mapFields(tt.input, model)
|
||||
err := mapFields(tt.input, model, "eu01")
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
@ -147,7 +150,7 @@ func TestEnableProject(t *testing.T) {
|
|||
client := &objectStorageClientMocked{
|
||||
returnError: tt.enableFails,
|
||||
}
|
||||
err := enableProject(context.Background(), &Model{}, client)
|
||||
err := enableProject(context.Background(), &Model{}, "eu01", client)
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
@ -288,7 +291,7 @@ func TestReadCredentialsGroups(t *testing.T) {
|
|||
ProjectId: tt.expectedModel.ProjectId,
|
||||
CredentialsGroupId: tt.expectedModel.CredentialsGroupId,
|
||||
}
|
||||
found, err := readCredentialsGroups(context.Background(), model, client)
|
||||
found, err := readCredentialsGroups(context.Background(), model, "eu01", client)
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error {
|
|||
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 {
|
||||
return fmt.Errorf("getting bucketsResp: %w", err)
|
||||
}
|
||||
|
|
@ -260,11 +260,11 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error {
|
|||
}
|
||||
bucketName := *bucket.Name
|
||||
if utils.Contains(bucketsToDestroy, bucketName) {
|
||||
_, err := client.DeleteBucketExecute(ctx, testutil.ProjectId, bucketName)
|
||||
_, err := client.DeleteBucketExecute(ctx, testutil.ProjectId, testutil.Region, bucketName)
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
credentialsGroupsResp, err := client.ListCredentialsGroups(ctx, testutil.ProjectId).Execute()
|
||||
credentialsGroupsResp, err := client.ListCredentialsGroups(ctx, testutil.ProjectId, testutil.Region).Execute()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting bucketsResp: %w", err)
|
||||
}
|
||||
|
|
@ -293,7 +293,7 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error {
|
|||
}
|
||||
groupId := *group.CredentialsGroupId
|
||||
if utils.Contains(credentialsGroupsToDestroy, groupId) {
|
||||
_, err := client.DeleteCredentialsGroupExecute(ctx, testutil.ProjectId, groupId)
|
||||
_, err := client.DeleteCredentialsGroupExecute(ctx, testutil.ProjectId, testutil.Region, groupId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("destroying credentials group %s during CheckDestroy: %w", groupId, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ var (
|
|||
OrganizationId = os.Getenv("TF_ACC_ORGANIZATION_ID")
|
||||
// ProjectId is the id of project used for tests
|
||||
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 = 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
|
||||
|
|
|
|||
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 {
|
||||
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