feat(scf): Add STACKIT Cloud Foundry (#991)

* onboard STACKIT Cloud Foundry resources/datasource
This commit is contained in:
Fabian Spottog 2025-10-08 11:42:33 +02:00 committed by GitHub
parent fcc7a99488
commit a8e874699f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 3700 additions and 0 deletions

View file

@ -0,0 +1,43 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_scf_organization Data Source - stackit"
subcategory: ""
description: |-
STACKIT Cloud Foundry organization datasource schema. Must have a region specified in the provider configuration.
---
# stackit_scf_organization (Data Source)
STACKIT Cloud Foundry organization datasource schema. Must have a `region` specified in the provider configuration.
## Example Usage
```terraform
data "stackit_scf_organization" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
org_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `org_id` (String) The ID of the Cloud Foundry Organization
- `project_id` (String) The ID of the project associated with the organization
### Optional
- `region` (String) The resource region. If not defined, the provider region is used
### Read-Only
- `created_at` (String) The time when the organization was created
- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`region`,`org_id`".
- `name` (String) The name of the organization
- `platform_id` (String) The ID of the platform associated with the organization
- `quota_id` (String) The ID of the quota associated with the organization
- `status` (String) The status of the organization (e.g., deleting, delete_failed)
- `suspended` (Boolean) A boolean indicating whether the organization is suspended
- `updated_at` (String) The time when the organization was last updated

View file

@ -0,0 +1,41 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_scf_organization_manager Data Source - stackit"
subcategory: ""
description: |-
STACKIT Cloud Foundry organization manager datasource schema.
---
# stackit_scf_organization_manager (Data Source)
STACKIT Cloud Foundry organization manager datasource schema.
## Example Usage
```terraform
data "stackit_scf_organization_manager" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
org_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `org_id` (String) The ID of the Cloud Foundry Organization
- `project_id` (String) The ID of the project associated with the organization of the organization manager
### Optional
- `region` (String) The region where the organization of the organization manager is located. If not defined, the provider region is used
### Read-Only
- `created_at` (String) The time when the organization manager was created
- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`region`,`org_id`,`user_id`".
- `platform_id` (String) The ID of the platform associated with the organization of the organization manager
- `updated_at` (String) The time when the organization manager was last updated
- `user_id` (String) The ID of the organization manager user
- `username` (String) An auto-generated organization manager user name

View file

@ -0,0 +1,40 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_scf_platform Data Source - stackit"
subcategory: ""
description: |-
STACKIT Cloud Foundry Platform datasource schema.
---
# stackit_scf_platform (Data Source)
STACKIT Cloud Foundry Platform datasource schema.
## Example Usage
```terraform
data "stackit_scf_platform" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
platform_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `platform_id` (String) The unique id of the platform
- `project_id` (String) The ID of the project associated with the platform
### Optional
- `region` (String) The region where the platform is located. If not defined, the provider region is used
### Read-Only
- `api_url` (String) The CF API Url of the platform
- `console_url` (String) The Stratos URL of the platform
- `display_name` (String) The name of the platform
- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`region`,`platform_id`".
- `system_id` (String) The ID of the platform System

View file

@ -0,0 +1,248 @@
# How to Provisioning Cloud Foundry using Terrform
## Objective
This tutorial demonstrates how to provision Cloud Foundry resources by
integrating the STACKIT Terraform provider with the Cloud Foundry Terraform
provider. The STACKIT Terraform provider will create a managed Cloud Foundry
organization and set up a technical "org manager" user with
`organization_manager` permissions. These credentials, along with the Cloud
Foundry API URL (retrieved dynamically from a platform data resource), are
passed to the Cloud Foundry Terraform provider to manage resources within the
new organization.
### Output
This configuration creates a Cloud Foundry organization, mirroring the structure
created via the portal. It sets up three distinct spaces: `dev`, `qa`, and
`prod`. The configuration assigns, a specified user the `organization_manager`
and `organization_user` roles at the organization level, and the
`space_developer` role in each space.
### Scope
This tutorial covers the interaction between the STACKIT Terraform provider and
the Cloud Foundry Terraform provider. It assumes you are familiar with:
- Setting up a STACKIT project and configuring the STACKIT Terraform provider
with a service account (see the general STACKIT documentation for details).
- Basic Terraform concepts, such as variables and locals.
This document does not cover foundational topics or every feature of the Cloud
Foundry Terraform provider.
### Example configuration
The following Terraform configuration provisions a Cloud Foundry organization
and related resources using the STACKIT Terraform provider and the Cloud Foundry
Terraform provider:
```
terraform {
required_providers {
stackit = {
source = "stackitcloud/stackit"
}
cloudfoundry = {
source = "cloudfoundry/cloudfoundry"
}
}
}
variable "project_id" {
type = string
description = "Id of the Project"
}
variable "org_name" {
type = string
description = "Name of the Organization"
}
variable "admin_email" {
type = string
description = "Users who are granted permissions"
}
provider "stackit" {
default_region = "eu01"
}
resource "stackit_scf_organization" "scf_org" {
name = var.org_name
project_id = var.project_id
}
data "stackit_scf_platform" "scf_platform" {
project_id = var.project_id
platform_id = stackit_scf_organization.scf_org.platform_id
}
resource "stackit_scf_organization_manager" "scf_manager" {
project_id = var.project_id
org_id = stackit_scf_organization.scf_org.org_id
}
provider "cloudfoundry" {
api_url = data.stackit_scf_platform.scf_platform.api_url
user = stackit_scf_organization_manager.scf_manager.username
password = stackit_scf_organization_manager.scf_manager.password
}
locals {
spaces = ["dev", "qa", "prod"]
}
resource "cloudfoundry_org_role" "org_user" {
username = var.admin_email
type = "organization_user"
org = stackit_scf_organization.scf_org.org_id
}
resource "cloudfoundry_org_role" "org_manager" {
username = var.admin_email
type = "organization_manager"
org = stackit_scf_organization.scf_org.org_id
}
resource "cloudfoundry_space" "spaces" {
for_each = toset(local.spaces)
name = each.key
org = stackit_scf_organization.scf_org.org_id
}
resource "cloudfoundry_space_role" "space_developer" {
for_each = toset(local.spaces)
username = var.admin_email
type = "space_developer"
depends_on = [ cloudfoundry_org_role.org_user ]
space = cloudfoundry_space.spaces[each.key].id
}
```
## Explanation of configuration
### STACKIT provider configuration
```
provider "stackit" {
default_region = "eu01"
}
```
The STACKIT Cloud Foundry Application Programming Interface (SCF API) is
regionalized. Each region operates independently. Set `default_region` in the
provider configuration, to specify the region for all resources, unless you
override it for individual resources. You must also provide access data for the
relevant STACKIT project for the provider to function.
For more details, see
the:[STACKIT Terraform Provider documentation.](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs)
### stackit_scf_organization.scf_org resource
```
resource "stackit_scf_organization" "scf_org" {
name = var.org_name
project_id = var.project_id
}
```
This resource provisions a Cloud Foundry organization, which acts as the
foundational container in the Cloud Foundry environment. Each Cloud Foundry
provider configuration is scoped to a specific organization. The organizations
name, defined by a variable, must be unique across the platform. The
organization is created within a designated STACKIT project, which requires the
STACKIT provider to be configured with the necessary permissions for that
project.
### stackit_scf_organization_manager.scf_manager resource
```
resource "stackit_scf_organization_manager" "scf_manager" {
project_id = var.project_id
org_id = stackit_scf_organization.scf_org.org_id
}
```
This resource creates a technical user in the Cloud Foundry organization with
the organization_manager permission. The user is linked to the organization and
is automatically deleted when the organization is removed.
### stackit_scf_platform.scf_platform data source
```
data "stackit_scf_platform" "scf_platform" {
project_id = var.project_id
platform_id = stackit_scf_organization.scf_org.platform_id
}
```
This data source retrieves properties of the Cloud Foundry platform where the
organization is provisioned. It does not create resources, but provides
information about the existing platform.
### Cloud Foundry provider configuration
```
provider "cloudfoundry" {
api_url = data.stackit_scf_platform.scf_platform.api_url
user = stackit_scf_organization_manager.scf_manager.username
password = stackit_scf_organization_manager.scf_manager.password
}
```
The Cloud Foundry provider is configured to manage resources in the new
organization. The provider uses the API URL from the `stackit_scf_platform` data
source and authenticates using the credentials of the technical user created by
the `stackit_scf_organization_manager` resource.
For more information, see the:
[Cloud Foundry Terraform Provider documentation.](https://registry.terraform.io/providers/cloudfoundry/cloudfoundry/latest/docs)
## Deploy resources
Follow these steps to initialize your environment and provision Cloud Foundry
resources using Terraform.
### Initialize Terraform
Run the following command to initialize the working directory and download the
required provider plugins:
```
terraform init
```
### Create the organization manager user
Run this command to provision the organization and technical user needed to
initialize the Cloud Foundry Terraform provider. This step is required only
during the initial setup. For later changes, you do not need the -target flag.
```
terraform apply -target stackit_scf_organization_manager.scf_manager
```
### Apply the full configuration
Run this command to provision all resources defined in your Terraform
configuration within the Cloud Foundry organization:
```
terraform apply
```
## Verify the deployment
Verify that your Cloud Foundry resources are provisioned correctly. Use the
following Cloud Foundry CLI commands to check applications, services, and
routes:
- `cf apps`
- `cf services`
- `cf routes`
For more information, see the
[Cloud Foundry documentation](https://docs.cloudfoundry.org/) and the
[Cloud Foundry CLI Reference Guide](https://cli.cloudfoundry.org/).

View file

@ -177,6 +177,7 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de
- `redis_custom_endpoint` (String) Custom endpoint for the Redis service
- `region` (String, Deprecated) Region will be used as the default location for regional services. Not all services require a region, some are global
- `resourcemanager_custom_endpoint` (String) Custom endpoint for the Resource Manager service
- `scf_custom_endpoint` (String) Custom endpoint for the Cloud Foundry (SCF) service
- `secretsmanager_custom_endpoint` (String) Custom endpoint for the Secrets Manager service
- `server_backup_custom_endpoint` (String) Custom endpoint for the Server Backup service
- `server_update_custom_endpoint` (String) Custom endpoint for the Server Update service

View file

@ -0,0 +1,57 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_scf_organization Resource - stackit"
subcategory: ""
description: |-
STACKIT Cloud Foundry organization resource schema. Must have a region specified in the provider configuration.
---
# stackit_scf_organization (Resource)
STACKIT Cloud Foundry organization resource schema. Must have a `region` specified in the provider configuration.
## Example Usage
```terraform
resource "stackit_scf_organization" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example"
}
resource "stackit_scf_organization" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example"
platform_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
quota_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
suspended = false
}
# Only use the import statement, if you want to import an existing scf organization
import {
to = stackit_scf_organization.import-example
id = "${var.project_id},${var.region},${var.org_id}"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) The name of the organization
- `project_id` (String) The ID of the project associated with the organization
### Optional
- `platform_id` (String) The ID of the platform associated with the organization
- `quota_id` (String) The ID of the quota associated with the organization
- `region` (String) The resource region. If not defined, the provider region is used
- `suspended` (Boolean) A boolean indicating whether the organization is suspended
### Read-Only
- `created_at` (String) The time when the organization was created
- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`region`,`org_id`".
- `org_id` (String) The ID of the Cloud Foundry Organization
- `status` (String) The status of the organization (e.g., deleting, delete_failed)
- `updated_at` (String) The time when the organization was last updated

View file

@ -0,0 +1,49 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_scf_organization_manager Resource - stackit"
subcategory: ""
description: |-
STACKIT Cloud Foundry organization manager resource schema.
---
# stackit_scf_organization_manager (Resource)
STACKIT Cloud Foundry organization manager resource schema.
## Example Usage
```terraform
resource "stackit_scf_organization_manager" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
org_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
# Only use the import statement, if you want to import an existing scf org user
# The password field is still null after import and must be entered manually in the state.
import {
to = stackit_scf_organization_manager.import-example
id = "${var.project_id},${var.region},${var.org_id},${var.user_id}"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `org_id` (String) The ID of the Cloud Foundry Organization
- `project_id` (String) The ID of the project associated with the organization of the organization manager
### Optional
- `region` (String) The region where the organization of the organization manager is located. If not defined, the provider region is used
### Read-Only
- `created_at` (String) The time when the organization manager was created
- `id` (String) Terraform's internal resource ID, structured as "`project_id`,`region`,`org_id`,`user_id`".
- `password` (String, Sensitive) An auto-generated password
- `platform_id` (String) The ID of the platform associated with the organization of the organization manager
- `updated_at` (String) The time when the organization manager was last updated
- `user_id` (String) The ID of the organization manager user
- `username` (String) An auto-generated organization manager user name

View file

@ -0,0 +1,4 @@
data "stackit_scf_organization" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
org_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

View file

@ -0,0 +1,4 @@
data "stackit_scf_organization_manager" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
org_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

View file

@ -0,0 +1,4 @@
data "stackit_scf_platform" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
platform_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

View file

@ -0,0 +1,18 @@
resource "stackit_scf_organization" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example"
}
resource "stackit_scf_organization" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example"
platform_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
quota_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
suspended = false
}
# Only use the import statement, if you want to import an existing scf organization
import {
to = stackit_scf_organization.import-example
id = "${var.project_id},${var.region},${var.org_id}"
}

View file

@ -0,0 +1,11 @@
resource "stackit_scf_organization_manager" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
org_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
# Only use the import statement, if you want to import an existing scf org user
# The password field is still null after import and must be entered manually in the state.
import {
to = stackit_scf_organization_manager.import-example
id = "${var.project_id},${var.region},${var.org_id},${var.user_id}"
}

1
go.mod
View file

@ -29,6 +29,7 @@ require (
github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1
github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1
github.com/stackitcloud/stackit-sdk-go/services/scf v0.2.1
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2
github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1

2
go.sum
View file

@ -190,6 +190,8 @@ github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1 h1:8uPt82Ez34OYMOi
github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1/go.mod h1:1Y2GEICmZDt+kr8aGnBx/sjYVAIYHmtfC8xYi9oxNEE=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1 h1:r7oaINTwLmIG31AaqKTuQHHFF8YNuYGzi+46DOuSjw4=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1/go.mod h1:ipcrPRbwfQXHH18dJVfY7K5ujHF5dTT6isoXgmA7YwQ=
github.com/stackitcloud/stackit-sdk-go/services/scf v0.2.1 h1:OdofRB6uj6lwN/TXLVHVrEOwNMG34MlFNwkiHD+eOts=
github.com/stackitcloud/stackit-sdk-go/services/scf v0.2.1/go.mod h1:5p7Xi8jadpJNDYr0t+07DXS104/RJLfhhA1r6P7PlGs=
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1 h1:WKFzlHllql3JsVcAq+Y1m5pSMkvwp1qH3Vf2N7i8CPg=
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1/go.mod h1:WGMFtGugBmUxI+nibI7eUZIQk4AGlDvwqX+m17W1y5w=
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2 h1:tfKC4Z6Uah9AQZrtCn/ytqOgc//ChQRfJ6ozxovgads=

View file

@ -45,6 +45,7 @@ type ProviderData struct {
RabbitMQCustomEndpoint string
RedisCustomEndpoint string
ResourceManagerCustomEndpoint string
ScfCustomEndpoint string
SecretsManagerCustomEndpoint string
SQLServerFlexCustomEndpoint string
ServerBackupCustomEndpoint string

View file

@ -0,0 +1,176 @@
package organization
import (
"context"
"fmt"
"net/http"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &scfOrganizationDataSource{}
_ datasource.DataSourceWithConfigure = &scfOrganizationDataSource{}
)
// NewScfOrganizationDataSource creates a new instance of the scfOrganizationDataSource.
func NewScfOrganizationDataSource() datasource.DataSource {
return &scfOrganizationDataSource{}
}
// scfOrganizationDataSource is the datasource implementation.
type scfOrganizationDataSource struct {
client *scf.APIClient
providerData core.ProviderData
}
func (s *scfOrganizationDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) {
var ok bool
s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics)
if !ok {
return
}
apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics)
if response.Diagnostics.HasError() {
return
}
s.client = apiClient
tflog.Info(ctx, "scf client configured")
}
func (s *scfOrganizationDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { // nolint:gocritic // function signature required by Terraform
response.TypeName = request.ProviderTypeName + "_scf_organization"
}
func (s *scfOrganizationDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { // nolint:gocritic // function signature required by Terraform
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
},
"created_at": schema.StringAttribute{
Description: descriptions["created_at"],
Computed: true,
},
"name": schema.StringAttribute{
Description: descriptions["name"],
Computed: true,
Validators: []validator.String{
stringvalidator.LengthBetween(1, 255),
},
},
"platform_id": schema.StringAttribute{
Description: descriptions["platform_id"],
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"org_id": schema.StringAttribute{
Description: descriptions["org_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"quota_id": schema.StringAttribute{
Description: descriptions["quota_id"],
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: descriptions["region"],
Optional: true,
Computed: true,
},
"status": schema.StringAttribute{
Description: descriptions["status"],
Computed: true,
},
"suspended": schema.BoolAttribute{
Description: descriptions["suspended"],
Computed: true,
},
"updated_at": schema.StringAttribute{
Description: descriptions["updated_at"],
Computed: true,
},
},
Description: "STACKIT Cloud Foundry organization datasource schema. Must have a `region` specified in the provider configuration.",
}
}
func (s *scfOrganizationDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve the current state of the resource.
var model Model
diags := request.Config.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
// Extract the project ID and instance id of the model
projectId := model.ProjectId.ValueString()
orgId := model.OrgId.ValueString()
// Extract the region
region := s.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "org_id", orgId)
ctx = tflog.SetField(ctx, "region", region)
// Read the current scf organization via orgId
scfOrgResponse, err := s.client.GetOrganization(ctx, projectId, region, orgId).Execute()
if err != nil {
utils.LogError(
ctx,
&response.Diagnostics,
err,
"Reading scf organization",
fmt.Sprintf("Organization with ID %q does not exist in project %q.", orgId, projectId),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", orgId),
},
)
response.State.RemoveResource(ctx)
return
}
err = mapFields(scfOrgResponse, &model)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization", fmt.Sprintf("Processing API response: %v", err))
return
}
// Set the updated state.
diags = response.State.Set(ctx, &model)
response.Diagnostics.Append(diags...)
tflog.Info(ctx, fmt.Sprintf("read scf organization %s", orgId))
}

View file

@ -0,0 +1,540 @@
package organization
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
"github.com/stackitcloud/stackit-sdk-go/services/scf/wait"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &scfOrganizationResource{}
_ resource.ResourceWithConfigure = &scfOrganizationResource{}
_ resource.ResourceWithImportState = &scfOrganizationResource{}
_ resource.ResourceWithModifyPlan = &scfOrganizationResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // Required by Terraform
CreateAt types.String `tfsdk:"created_at"`
Name types.String `tfsdk:"name"`
PlatformId types.String `tfsdk:"platform_id"`
ProjectId types.String `tfsdk:"project_id"`
QuotaId types.String `tfsdk:"quota_id"`
OrgId types.String `tfsdk:"org_id"`
Region types.String `tfsdk:"region"`
Status types.String `tfsdk:"status"`
Suspended types.Bool `tfsdk:"suspended"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
// NewScfOrganizationResource is a helper function to create a new scf organization resource.
func NewScfOrganizationResource() resource.Resource {
return &scfOrganizationResource{}
}
// scfOrganizationResource implements the resource interface for scf organization.
type scfOrganizationResource struct {
client *scf.APIClient
providerData core.ProviderData
}
// descriptions for the attributes in the Schema
var descriptions = map[string]string{
"id": "Terraform's internal resource ID, structured as \"`project_id`,`region`,`org_id`\".",
"created_at": "The time when the organization was created",
"name": "The name of the organization",
"platform_id": "The ID of the platform associated with the organization",
"project_id": "The ID of the project associated with the organization",
"quota_id": "The ID of the quota associated with the organization",
"region": "The resource region. If not defined, the provider region is used",
"status": "The status of the organization (e.g., deleting, delete_failed)",
"suspended": "A boolean indicating whether the organization is suspended",
"org_id": "The ID of the Cloud Foundry Organization",
"updated_at": "The time when the organization was last updated",
}
func (s *scfOrganizationResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) {
var ok bool
s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics)
if !ok {
return
}
apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics)
if response.Diagnostics.HasError() {
return
}
s.client = apiClient
tflog.Info(ctx, "scf client configured")
}
func (s *scfOrganizationResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
response.TypeName = request.ProviderTypeName + "_scf_organization"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *scfOrganizationResource) 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.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
func (s *scfOrganizationResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) {
response.Schema = schema.Schema{
Description: "STACKIT Cloud Foundry organization resource schema. Must have a `region` specified in the provider configuration.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"created_at": schema.StringAttribute{
Description: descriptions["created_at"],
Computed: true,
},
"name": schema.StringAttribute{
Description: descriptions["name"],
Required: true,
Validators: []validator.String{
stringvalidator.LengthBetween(1, 255),
},
},
"platform_id": schema.StringAttribute{
Description: descriptions["platform_id"],
Optional: true,
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplace(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"org_id": schema.StringAttribute{
Description: descriptions["org_id"],
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"quota_id": schema.StringAttribute{
Description: descriptions["quota_id"],
Optional: true,
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"region": schema.StringAttribute{
Description: descriptions["region"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplace(),
},
},
"status": schema.StringAttribute{
Description: descriptions["status"],
Computed: true,
},
"suspended": schema.BoolAttribute{
Description: descriptions["suspended"],
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"updated_at": schema.StringAttribute{
Description: descriptions["updated_at"],
Computed: true,
},
},
}
}
func (s *scfOrganizationResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve the planned values for the resource.
var model Model
diags := request.Plan.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
// Set logging context with the project ID and instance ID.
region := model.Region.ValueString()
projectId := model.ProjectId.ValueString()
orgName := model.Name.ValueString()
quotaId := model.QuotaId.ValueString()
suspended := model.Suspended.ValueBool()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "org_name", orgName)
ctx = tflog.SetField(ctx, "region", region)
payload, err := toCreatePayload(&model)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Creating API payload: %v\n", err))
return
}
// Create the new scf organization via the API client.
scfOrgCreateResponse, err := s.client.CreateOrganization(ctx, projectId, region).
CreateOrganizationPayload(payload).
Execute()
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Calling API to create org: %v", err))
return
}
orgId := *scfOrgCreateResponse.Guid
// Apply the org quota if provided
if quotaId != "" {
applyOrgQuota, err := s.client.ApplyOrganizationQuota(ctx, projectId, region, orgId).ApplyOrganizationQuotaPayload(
scf.ApplyOrganizationQuotaPayload{
QuotaId: &quotaId,
}).Execute()
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Calling API to apply quota: %v", err))
return
}
model.QuotaId = types.StringPointerValue(applyOrgQuota.QuotaId)
}
if suspended {
_, err := s.client.UpdateOrganization(ctx, projectId, region, orgId).UpdateOrganizationPayload(
scf.UpdateOrganizationPayload{
Suspended: &suspended,
}).Execute()
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Calling API to update suspended: %v", err))
return
}
}
// Load the newly created scf organization
scfOrgResponse, err := s.client.GetOrganization(ctx, projectId, region, orgId).Execute()
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Calling API to load created org: %v", err))
return
}
err = mapFields(scfOrgResponse, &model)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set the state with fully populated data.
diags = response.State.Set(ctx, model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Scf organization created")
}
// Read refreshes the Terraform state with the latest scf organization data.
func (s *scfOrganizationResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve the current state of the resource.
var model Model
diags := request.State.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
// Extract the project ID and instance id of the model
projectId := model.ProjectId.ValueString()
orgId := model.OrgId.ValueString()
// Extract the region
region := s.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "org_id", orgId)
ctx = tflog.SetField(ctx, "region", region)
// Read the current scf organization via guid
scfOrgResponse, err := s.client.GetOrganization(ctx, projectId, region, orgId).Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if ok && oapiErr.StatusCode == http.StatusNotFound {
response.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization", fmt.Sprintf("Calling API: %v", err))
return
}
err = mapFields(scfOrgResponse, &model)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization", fmt.Sprintf("Processing API response: %v", err))
return
}
// Set the updated state.
diags = response.State.Set(ctx, &model)
response.Diagnostics.Append(diags...)
tflog.Info(ctx, fmt.Sprintf("read scf organization %s", orgId))
}
// Update attempts to update the resource.
func (s *scfOrganizationResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
diags := request.Plan.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
region := model.Region.ValueString()
projectId := model.ProjectId.ValueString()
orgId := model.OrgId.ValueString()
name := model.Name.ValueString()
quotaId := model.QuotaId.ValueString()
suspended := model.Suspended.ValueBool()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "org_id", orgId)
ctx = tflog.SetField(ctx, "region", region)
org, err := s.client.GetOrganization(ctx, projectId, region, orgId).Execute()
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error retrieving organization state", fmt.Sprintf("Getting organization state: %v", err))
return
}
// handle a change of the organization name or the suspended flag
if name != org.GetName() || suspended != org.GetSuspended() {
updatedOrg, err := s.client.UpdateOrganization(ctx, projectId, region, orgId).UpdateOrganizationPayload(
scf.UpdateOrganizationPayload{
Name: &name,
Suspended: &suspended,
}).Execute()
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error updating organization", fmt.Sprintf("Processing API payload: %v", err))
return
}
org = updatedOrg
}
// handle a quota change of the org
if quotaId != org.GetQuotaId() {
applyOrgQuota, err := s.client.ApplyOrganizationQuota(ctx, projectId, region, orgId).ApplyOrganizationQuotaPayload(
scf.ApplyOrganizationQuotaPayload{
QuotaId: &quotaId,
}).Execute()
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error applying organization quota", fmt.Sprintf("Processing API payload: %v", err))
return
}
org.QuotaId = applyOrgQuota.QuotaId
}
err = mapFields(org, &model)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error updating organization", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = response.State.Set(ctx, model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "organization updated")
}
// Delete deletes the git instance and removes it from the Terraform state on success.
func (s *scfOrganizationResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve current state of the resource.
var model Model
diags := request.State.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
orgId := model.OrgId.ValueString()
// Extract the region
region := model.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "org_id", orgId)
ctx = tflog.SetField(ctx, "region", region)
// Call API to delete the existing scf organization.
_, err := s.client.DeleteOrganization(ctx, projectId, region, orgId).Execute()
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error deleting scf organization", fmt.Sprintf("Calling API: %v", err))
return
}
_, err = wait.DeleteOrganizationWaitHandler(ctx, s.client, projectId, model.Region.ValueString(), orgId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error waiting for scf org deletion", fmt.Sprintf("SCFOrganization deleting waiting: %v", err))
return
}
tflog.Info(ctx, "Scf organization deleted")
}
func (s *scfOrganizationResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
// Split the import identifier to extract project ID and email.
idParts := strings.Split(request.ID, core.Separator)
// Ensure the import identifier format is correct.
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &response.Diagnostics,
"Error importing scf organization",
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[org_id] Got: %q", request.ID),
)
return
}
projectId := idParts[0]
region := idParts[1]
orgId := idParts[2]
// Set the project id and organization id in the state
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("region"), region)...)
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("org_id"), orgId)...)
tflog.Info(ctx, "Scf organization state imported")
}
// mapFields maps a SCF Organization response to the model.
func mapFields(response *scf.Organization, model *Model) error {
if response == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var orgId string
if response.Guid != nil {
orgId = *response.Guid
} else if model.OrgId.ValueString() != "" {
orgId = model.OrgId.ValueString()
} else {
return fmt.Errorf("org id is not present")
}
var projectId string
if response.ProjectId != nil {
projectId = *response.ProjectId
} else if model.ProjectId.ValueString() != "" {
projectId = model.ProjectId.ValueString()
} else {
return fmt.Errorf("project id is not present")
}
var region string
if response.Region != nil {
region = *response.Region
} else if model.Region.ValueString() != "" {
region = model.Region.ValueString()
} else {
return fmt.Errorf("region is not present")
}
// Build the ID by combining the project ID and organization id and assign the model's fields.
model.Id = utils.BuildInternalTerraformId(projectId, region, orgId)
model.ProjectId = types.StringValue(projectId)
model.Region = types.StringValue(region)
model.PlatformId = types.StringPointerValue(response.PlatformId)
model.OrgId = types.StringValue(orgId)
model.Name = types.StringPointerValue(response.Name)
model.Status = types.StringPointerValue(response.Status)
model.Suspended = types.BoolPointerValue(response.Suspended)
model.QuotaId = types.StringPointerValue(response.QuotaId)
model.CreateAt = types.StringValue(response.CreatedAt.String())
model.UpdatedAt = types.StringValue(response.UpdatedAt.String())
return nil
}
// toCreatePayload creates the payload to create a scf organization instance
func toCreatePayload(model *Model) (scf.CreateOrganizationPayload, error) {
if model == nil {
return scf.CreateOrganizationPayload{}, fmt.Errorf("nil model")
}
payload := scf.CreateOrganizationPayload{
Name: model.Name.ValueStringPointer(),
}
if !model.PlatformId.IsNull() && !model.PlatformId.IsUnknown() {
payload.PlatformId = model.PlatformId.ValueStringPointer()
}
return payload, nil
}

View file

@ -0,0 +1,177 @@
package organization
import (
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
)
var (
testOrgId = uuid.New().String()
testProjectId = uuid.New().String()
testPlatformId = uuid.New().String()
testQuotaId = uuid.New().String()
testRegion = "eu01"
)
func TestMapFields(t *testing.T) {
createdTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", "2025-01-01 00:00:00 +0000 UTC")
if err != nil {
t.Fatalf("failed to parse test time: %v", err)
}
tests := []struct {
description string
input *scf.Organization
expected *Model
isValid bool
}{
{
description: "minimal_input",
input: &scf.Organization{
Guid: utils.Ptr(testOrgId),
Name: utils.Ptr("scf-org-min-instance"),
Region: utils.Ptr(testRegion),
CreatedAt: &createdTime,
UpdatedAt: &createdTime,
ProjectId: utils.Ptr(testProjectId),
},
expected: &Model{
Id: types.StringValue(fmt.Sprintf("%s,%s,%s", testProjectId, testRegion, testOrgId)),
ProjectId: types.StringValue(testProjectId),
Region: types.StringValue(testRegion),
Name: types.StringValue("scf-org-min-instance"),
PlatformId: types.StringNull(),
OrgId: types.StringValue(testOrgId),
QuotaId: types.StringNull(),
Status: types.StringNull(),
Suspended: types.BoolNull(),
CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
},
isValid: true,
},
{
description: "max_input",
input: &scf.Organization{
CreatedAt: &createdTime,
Guid: utils.Ptr(testOrgId),
Name: utils.Ptr("scf-full-org"),
PlatformId: utils.Ptr(testPlatformId),
ProjectId: utils.Ptr(testProjectId),
QuotaId: utils.Ptr(testQuotaId),
Region: utils.Ptr(testRegion),
Status: nil,
Suspended: utils.Ptr(true),
UpdatedAt: &createdTime,
},
expected: &Model{
Id: types.StringValue(fmt.Sprintf("%s,%s,%s", testProjectId, testRegion, testOrgId)),
ProjectId: types.StringValue(testProjectId),
OrgId: types.StringValue(testOrgId),
Name: types.StringValue("scf-full-org"),
Region: types.StringValue(testRegion),
PlatformId: types.StringValue(testPlatformId),
CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
QuotaId: types.StringValue(testQuotaId),
Status: types.StringNull(),
Suspended: types.BoolValue(true),
},
isValid: true,
},
{
description: "nil_org",
input: nil,
expected: nil,
isValid: false,
},
{
description: "empty_org",
input: &scf.Organization{},
expected: nil,
isValid: false,
},
{
description: "missing_id",
input: &scf.Organization{
Name: utils.Ptr("scf-missing-id"),
},
expected: nil,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
state := &Model{}
if tt.expected != nil {
state.ProjectId = tt.expected.ProjectId
}
err := mapFields(tt.input, state)
if tt.isValid && err != nil {
t.Fatalf("expected success, got error: %v", err)
}
if !tt.isValid && err == nil {
t.Fatalf("expected error, got nil")
}
if tt.isValid {
if diff := cmp.Diff(tt.expected, state); diff != "" {
t.Errorf("unexpected diff (-want +got):\n%s", diff)
}
}
})
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected scf.CreateOrganizationPayload
expectError bool
}{
{
description: "default values",
input: &Model{
Name: types.StringValue("example-org"),
PlatformId: types.StringValue(testPlatformId),
},
expected: scf.CreateOrganizationPayload{
Name: utils.Ptr("example-org"),
PlatformId: utils.Ptr(testPlatformId),
},
expectError: false,
},
{
description: "nil input model",
input: nil,
expected: scf.CreateOrganizationPayload{},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toCreatePayload(tt.input)
if tt.expectError && err == nil {
t.Fatalf("expected diagnostics error but got none")
}
if !tt.expectError && err != nil {
t.Fatalf("unexpected diagnostics error: %v", err)
}
if diff := cmp.Diff(tt.expected, output); diff != "" {
t.Fatalf("unexpected payload (-want +got):\n%s", diff)
}
})
}
}

View file

@ -0,0 +1,238 @@
package organizationmanager
import (
"context"
"fmt"
"net/http"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &scfOrganizationManagerDataSource{}
_ datasource.DataSourceWithConfigure = &scfOrganizationManagerDataSource{}
)
type DataSourceModel struct {
Id types.String `tfsdk:"id"` // Required by Terraform
Region types.String `tfsdk:"region"`
PlatformId types.String `tfsdk:"platform_id"`
ProjectId types.String `tfsdk:"project_id"`
OrgId types.String `tfsdk:"org_id"`
UserId types.String `tfsdk:"user_id"`
UserName types.String `tfsdk:"username"`
CreateAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
// NewScfOrganizationManagerDataSource creates a new instance of the scfOrganizationDataSource.
func NewScfOrganizationManagerDataSource() datasource.DataSource {
return &scfOrganizationManagerDataSource{}
}
// scfOrganizationManagerDataSource is the datasource implementation.
type scfOrganizationManagerDataSource struct {
client *scf.APIClient
providerData core.ProviderData
}
func (s *scfOrganizationManagerDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) {
var ok bool
s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics)
if !ok {
return
}
apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics)
if response.Diagnostics.HasError() {
return
}
s.client = apiClient
tflog.Info(ctx, "scf client configured for scfOrganizationManagerDataSource")
}
func (s *scfOrganizationManagerDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { // nolint:gocritic // function signature required by Terraform
response.TypeName = request.ProviderTypeName + "_scf_organization_manager"
}
func (s *scfOrganizationManagerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { // nolint:gocritic // function signature required by Terraform
response.Schema = schema.Schema{
Description: "STACKIT Cloud Foundry organization manager datasource schema.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
},
"region": schema.StringAttribute{
Description: descriptions["region"],
Optional: true,
Computed: true,
},
"platform_id": schema.StringAttribute{
Description: descriptions["platform_id"],
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"org_id": schema.StringAttribute{
Description: descriptions["org_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"user_id": schema.StringAttribute{
Description: descriptions["user_id"],
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"username": schema.StringAttribute{
Description: descriptions["username"],
Computed: true,
Validators: []validator.String{
stringvalidator.LengthBetween(1, 255),
},
},
"created_at": schema.StringAttribute{
Description: descriptions["created_at"],
Computed: true,
},
"updated_at": schema.StringAttribute{
Description: descriptions["updated_at"],
Computed: true,
},
},
}
}
func (s *scfOrganizationManagerDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve the current state of the resource.
var model DataSourceModel
diags := request.Config.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
// Extract the project ID and instance id of the model
projectId := model.ProjectId.ValueString()
orgId := model.OrgId.ValueString()
region := s.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "org_id", orgId)
ctx = tflog.SetField(ctx, "region", region)
// Read the current scf organization manager via orgId
ScfOrgManager, err := s.client.GetOrgManagerExecute(ctx, projectId, region, orgId)
if err != nil {
utils.LogError(
ctx,
&response.Diagnostics,
err,
"Reading scf organization manager",
fmt.Sprintf("Organization with ID %q does not exist in project %q.", orgId, projectId),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", orgId),
},
)
response.State.RemoveResource(ctx)
return
}
err = mapFieldsDataSource(ScfOrgManager, &model)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization manager", fmt.Sprintf("Processing API response: %v", err))
return
}
// Set the updated state.
diags = response.State.Set(ctx, &model)
response.Diagnostics.Append(diags...)
tflog.Info(ctx, fmt.Sprintf("read scf organization manager %s", orgId))
}
func mapFieldsDataSource(response *scf.OrgManager, model *DataSourceModel) error {
if response == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var projectId string
if response.ProjectId != nil {
projectId = *response.ProjectId
} else if model.ProjectId.ValueString() != "" {
projectId = model.ProjectId.ValueString()
} else {
return fmt.Errorf("project id is not present")
}
var region string
if response.Region != nil {
region = *response.Region
} else if model.Region.ValueString() != "" {
region = model.Region.ValueString()
} else {
return fmt.Errorf("region is not present")
}
var orgId string
if response.OrgId != nil {
orgId = *response.OrgId
} else if model.OrgId.ValueString() != "" {
orgId = model.OrgId.ValueString()
} else {
return fmt.Errorf("org id is not present")
}
var userId string
if response.Guid != nil {
userId = *response.Guid
if model.UserId.ValueString() != "" && userId != model.UserId.ValueString() {
return fmt.Errorf("user id mismatch in response and model")
}
} else if model.UserId.ValueString() != "" {
userId = model.UserId.ValueString()
} else {
return fmt.Errorf("user id is not present")
}
model.Id = utils.BuildInternalTerraformId(projectId, region, orgId, userId)
model.Region = types.StringValue(region)
model.PlatformId = types.StringPointerValue(response.PlatformId)
model.ProjectId = types.StringValue(projectId)
model.OrgId = types.StringValue(orgId)
model.UserId = types.StringValue(userId)
model.UserName = types.StringPointerValue(response.Username)
model.CreateAt = types.StringValue(response.CreatedAt.String())
model.UpdatedAt = types.StringValue(response.UpdatedAt.String())
return nil
}

View file

@ -0,0 +1,116 @@
package organizationmanager
import (
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
)
func TestMapFieldsDataSource(t *testing.T) {
createdTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", "2025-01-01 00:00:00 +0000 UTC")
if err != nil {
t.Fatalf("failed to parse test time: %v", err)
}
tests := []struct {
description string
input *scf.OrgManager
expected *DataSourceModel
isValid bool
}{
{
description: "minimal_input",
input: &scf.OrgManager{
Guid: utils.Ptr(testUserId),
OrgId: utils.Ptr(testOrgId),
ProjectId: utils.Ptr(testProjectId),
Region: utils.Ptr(testRegion),
CreatedAt: &createdTime,
UpdatedAt: &createdTime,
},
expected: &DataSourceModel{
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)),
UserId: types.StringValue(testUserId),
OrgId: types.StringValue(testOrgId),
ProjectId: types.StringValue(testProjectId),
Region: types.StringValue(testRegion),
UserName: types.StringNull(),
PlatformId: types.StringNull(),
CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
},
isValid: true,
},
{
description: "max_input",
input: &scf.OrgManager{
Guid: utils.Ptr(testUserId),
OrgId: utils.Ptr(testOrgId),
ProjectId: utils.Ptr(testProjectId),
PlatformId: utils.Ptr(testPlatformId),
Region: utils.Ptr(testRegion),
CreatedAt: &createdTime,
UpdatedAt: &createdTime,
Username: utils.Ptr("test-user"),
},
expected: &DataSourceModel{
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)),
UserId: types.StringValue(testUserId),
OrgId: types.StringValue(testOrgId),
ProjectId: types.StringValue(testProjectId),
PlatformId: types.StringValue(testPlatformId),
Region: types.StringValue(testRegion),
UserName: types.StringValue("test-user"),
CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
},
isValid: true,
},
{
description: "nil_org",
input: nil,
expected: nil,
isValid: false,
},
{
description: "empty_org",
input: &scf.OrgManager{},
expected: nil,
isValid: false,
},
{
description: "missing_id",
input: &scf.OrgManager{
Username: utils.Ptr("scf-missing-id"),
},
expected: nil,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
state := &DataSourceModel{}
if tt.expected != nil {
state.ProjectId = tt.expected.ProjectId
}
err := mapFieldsDataSource(tt.input, state)
if tt.isValid && err != nil {
t.Fatalf("expected success, got error: %v", err)
}
if !tt.isValid && err == nil {
t.Fatalf("expected error, got nil")
}
if tt.isValid {
if diff := cmp.Diff(tt.expected, state); diff != "" {
t.Errorf("unexpected diff (-want +got):\n%s", diff)
}
}
})
}
}

View file

@ -0,0 +1,471 @@
package organizationmanager
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &scfOrganizationManagerResource{}
_ resource.ResourceWithConfigure = &scfOrganizationManagerResource{}
_ resource.ResourceWithImportState = &scfOrganizationManagerResource{}
_ resource.ResourceWithModifyPlan = &scfOrganizationManagerResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // Required by Terraform
Region types.String `tfsdk:"region"`
PlatformId types.String `tfsdk:"platform_id"`
ProjectId types.String `tfsdk:"project_id"`
OrgId types.String `tfsdk:"org_id"`
UserId types.String `tfsdk:"user_id"`
UserName types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
CreateAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
}
// NewScfOrganizationManagerResource is a helper function to create a new scf organization manager resource.
func NewScfOrganizationManagerResource() resource.Resource {
return &scfOrganizationManagerResource{}
}
// scfOrganizationManagerResource implements the resource interface for scf organization manager.
type scfOrganizationManagerResource struct {
client *scf.APIClient
providerData core.ProviderData
}
// descriptions for the attributes in the Schema
var descriptions = map[string]string{
"id": "Terraform's internal resource ID, structured as \"`project_id`,`region`,`org_id`,`user_id`\".",
"region": "The region where the organization of the organization manager is located. If not defined, the provider region is used",
"platform_id": "The ID of the platform associated with the organization of the organization manager",
"project_id": "The ID of the project associated with the organization of the organization manager",
"org_id": "The ID of the Cloud Foundry Organization",
"user_id": "The ID of the organization manager user",
"username": "An auto-generated organization manager user name",
"password": "An auto-generated password",
"created_at": "The time when the organization manager was created",
"updated_at": "The time when the organization manager was last updated",
}
func (s *scfOrganizationManagerResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { // nolint:gocritic // function signature required by Terraform
var ok bool
s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics)
if !ok {
return
}
apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics)
if response.Diagnostics.HasError() {
return
}
s.client = apiClient
tflog.Info(ctx, "scf client configured")
}
func (s *scfOrganizationManagerResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { // nolint:gocritic // function signature required by Terraform
response.TypeName = request.ProviderTypeName + "_scf_organization_manager"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *scfOrganizationManagerResource) 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.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
func (s *scfOrganizationManagerResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { // nolint:gocritic // function signature required by Terraform
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
},
"region": schema.StringAttribute{
Description: descriptions["region"],
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplace(),
},
},
"platform_id": schema.StringAttribute{
Description: descriptions["platform_id"],
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplace(),
},
},
"org_id": schema.StringAttribute{
Description: descriptions["org_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplace(),
},
},
"user_id": schema.StringAttribute{
Description: descriptions["user_id"],
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"username": schema.StringAttribute{
Description: descriptions["username"],
Computed: true,
Validators: []validator.String{
stringvalidator.LengthBetween(1, 255),
},
},
"password": schema.StringAttribute{
Description: descriptions["password"],
Computed: true,
Sensitive: true,
Validators: []validator.String{
stringvalidator.LengthBetween(1, 255),
},
},
"created_at": schema.StringAttribute{
Description: descriptions["created_at"],
Computed: true,
},
"updated_at": schema.StringAttribute{
Description: descriptions["updated_at"],
Computed: true,
},
},
Description: "STACKIT Cloud Foundry organization manager resource schema.",
}
}
func (s *scfOrganizationManagerResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve the planned values for the resource.
var model Model
diags := request.Plan.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
// Set logging context with the project ID and username.
projectId := model.ProjectId.ValueString()
orgId := model.OrgId.ValueString()
userName := model.UserName.ValueString()
region := model.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "username", userName)
ctx = tflog.SetField(ctx, "region", region)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
// Create the new scf organization manager via the API client.
scfOrgManagerCreateResponse, err := s.client.CreateOrgManagerExecute(ctx, projectId, region, orgId)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization manager", fmt.Sprintf("Calling API to create org manager: %v", err))
return
}
err = mapFieldsCreate(scfOrgManagerCreateResponse, &model)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization manager", fmt.Sprintf("Mapping fields: %v", err))
return
}
// Set the state with fully populated data.
diags = response.State.Set(ctx, model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Scf organization manager created")
}
func (s *scfOrganizationManagerResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve the current state of the resource.
var model Model
diags := request.State.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
// Extract the project ID, region and org id of the model
projectId := model.ProjectId.ValueString()
orgId := model.OrgId.ValueString()
region := s.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "org_id", orgId)
ctx = tflog.SetField(ctx, "region", region)
// Read the current scf organization manager via orgId
scfOrgManager, err := s.client.GetOrgManagerExecute(ctx, projectId, region, orgId)
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if ok && oapiErr.StatusCode == http.StatusNotFound {
core.LogAndAddWarning(ctx, &response.Diagnostics, "SCF Organization manager not found", "SCF Organization manager not found, remove from state")
response.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization manager", fmt.Sprintf("Calling API: %v", err))
return
}
err = mapFieldsRead(scfOrgManager, &model)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization manager", fmt.Sprintf("Processing API response: %v", err))
return
}
// Set the updated state.
diags = response.State.Set(ctx, &model)
response.Diagnostics.Append(diags...)
tflog.Info(ctx, fmt.Sprintf("read scf organization manager %s", orgId))
}
func (s *scfOrganizationManagerResource) Update(ctx context.Context, _ resource.UpdateRequest, response *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// organization manager cannot be updated, so we log an error.
core.LogAndAddError(ctx, &response.Diagnostics, "Error updating organization manager", "Organization Manager can't be updated")
}
func (s *scfOrganizationManagerResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve current state of the resource.
var model Model
diags := request.State.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
orgId := model.OrgId.ValueString()
region := model.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "org_id", orgId)
ctx = tflog.SetField(ctx, "region", region)
// Call API to delete the existing scf organization manager.
_, err := s.client.DeleteOrgManagerExecute(ctx, projectId, region, orgId)
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if ok && oapiErr.StatusCode == http.StatusGone {
tflog.Info(ctx, "Scf organization manager was already deleted")
return
}
core.LogAndAddError(ctx, &response.Diagnostics, "Error deleting scf organization manager", fmt.Sprintf("Calling API: %v", err))
return
}
tflog.Info(ctx, "Scf organization manager deleted")
}
func (s *scfOrganizationManagerResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { // nolint:gocritic // function signature required by Terraform
// Split the import identifier to extract project ID, region org ID and user ID.
idParts := strings.Split(request.ID, core.Separator)
// Ensure the import identifier format is correct.
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(ctx, &response.Diagnostics,
"Error importing scf organization manager",
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[org_id],[user_id] Got: %q", request.ID),
)
return
}
projectId := idParts[0]
region := idParts[1]
orgId := idParts[2]
userId := idParts[3]
// Set the project id, region organization id and user id in the state
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("region"), region)...)
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("org_id"), orgId)...)
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("user_id"), userId)...)
tflog.Info(ctx, "Scf organization manager state imported")
}
func mapFieldsCreate(response *scf.OrgManagerResponse, model *Model) error {
if response == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var projectId string
if response.ProjectId != nil {
projectId = *response.ProjectId
} else if model.ProjectId.ValueString() != "" {
projectId = model.ProjectId.ValueString()
} else {
return fmt.Errorf("project id is not present")
}
var region string
if response.Region != nil {
region = *response.Region
} else if model.Region.ValueString() != "" {
region = model.Region.ValueString()
} else {
return fmt.Errorf("region is not present")
}
var orgId string
if response.OrgId != nil {
orgId = *response.OrgId
} else if model.OrgId.ValueString() != "" {
orgId = model.OrgId.ValueString()
} else {
return fmt.Errorf("org id is not present")
}
var userId string
if response.Guid != nil {
userId = *response.Guid
} else if model.UserId.ValueString() != "" {
userId = model.UserId.ValueString()
} else {
return fmt.Errorf("user id is not present")
}
model.Id = utils.BuildInternalTerraformId(projectId, region, orgId, userId)
model.Region = types.StringValue(region)
model.PlatformId = types.StringPointerValue(response.PlatformId)
model.ProjectId = types.StringValue(projectId)
model.OrgId = types.StringValue(orgId)
model.UserId = types.StringValue(userId)
model.UserName = types.StringPointerValue(response.Username)
model.Password = types.StringPointerValue(response.Password)
model.CreateAt = types.StringValue(response.CreatedAt.String())
model.UpdatedAt = types.StringValue(response.UpdatedAt.String())
return nil
}
func mapFieldsRead(response *scf.OrgManager, model *Model) error {
if response == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var projectId string
if response.ProjectId != nil {
projectId = *response.ProjectId
} else if model.ProjectId.ValueString() != "" {
projectId = model.ProjectId.ValueString()
} else {
return fmt.Errorf("project id is not present")
}
var region string
if response.Region != nil {
region = *response.Region
} else if model.Region.ValueString() != "" {
region = model.Region.ValueString()
} else {
return fmt.Errorf("region is not present")
}
var orgId string
if response.OrgId != nil {
orgId = *response.OrgId
} else if model.OrgId.ValueString() != "" {
orgId = model.OrgId.ValueString()
} else {
return fmt.Errorf("org id is not present")
}
var userId string
if response.Guid != nil {
userId = *response.Guid
if model.UserId.ValueString() != "" && userId != model.UserId.ValueString() {
return fmt.Errorf("user id mismatch in response and model")
}
} else if model.UserId.ValueString() != "" {
userId = model.UserId.ValueString()
} else {
return fmt.Errorf("user id is not present")
}
model.Id = utils.BuildInternalTerraformId(projectId, region, orgId, userId)
model.Region = types.StringValue(region)
model.PlatformId = types.StringPointerValue(response.PlatformId)
model.ProjectId = types.StringValue(projectId)
model.OrgId = types.StringValue(orgId)
model.UserId = types.StringValue(userId)
model.UserName = types.StringPointerValue(response.Username)
model.CreateAt = types.StringValue(response.CreatedAt.String())
model.UpdatedAt = types.StringValue(response.UpdatedAt.String())
return nil
}

View file

@ -0,0 +1,233 @@
package organizationmanager
import (
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
)
var (
testOrgId = uuid.New().String()
testProjectId = uuid.New().String()
testPlatformId = uuid.New().String()
testUserId = uuid.New().String()
testRegion = "eu01"
)
func TestMapFields(t *testing.T) {
createdTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", "2025-01-01 00:00:00 +0000 UTC")
if err != nil {
t.Fatalf("failed to parse test time: %v", err)
}
tests := []struct {
description string
input *scf.OrgManager
expected *Model
isValid bool
}{
{
description: "minimal_input",
input: &scf.OrgManager{
Guid: utils.Ptr(testUserId),
OrgId: utils.Ptr(testOrgId),
ProjectId: utils.Ptr(testProjectId),
Region: utils.Ptr(testRegion),
CreatedAt: &createdTime,
UpdatedAt: &createdTime,
},
expected: &Model{
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)),
UserId: types.StringValue(testUserId),
OrgId: types.StringValue(testOrgId),
ProjectId: types.StringValue(testProjectId),
Region: types.StringValue(testRegion),
UserName: types.StringNull(),
PlatformId: types.StringNull(),
CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
},
isValid: true,
},
{
description: "max_input",
input: &scf.OrgManager{
Guid: utils.Ptr(testUserId),
OrgId: utils.Ptr(testOrgId),
ProjectId: utils.Ptr(testProjectId),
PlatformId: utils.Ptr(testPlatformId),
Region: utils.Ptr(testRegion),
CreatedAt: &createdTime,
UpdatedAt: &createdTime,
Username: utils.Ptr("test-user"),
},
expected: &Model{
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)),
UserId: types.StringValue(testUserId),
OrgId: types.StringValue(testOrgId),
ProjectId: types.StringValue(testProjectId),
PlatformId: types.StringValue(testPlatformId),
Region: types.StringValue(testRegion),
Password: types.StringNull(),
UserName: types.StringValue("test-user"),
CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
},
isValid: true,
},
{
description: "nil_org",
input: nil,
expected: nil,
isValid: false,
},
{
description: "empty_org",
input: &scf.OrgManager{},
expected: nil,
isValid: false,
},
{
description: "missing_id",
input: &scf.OrgManager{
Username: utils.Ptr("scf-missing-id"),
},
expected: nil,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
state := &Model{}
if tt.expected != nil {
state.ProjectId = tt.expected.ProjectId
}
err := mapFieldsRead(tt.input, state)
if tt.isValid && err != nil {
t.Fatalf("expected success, got error: %v", err)
}
if !tt.isValid && err == nil {
t.Fatalf("expected error, got nil")
}
if tt.isValid {
if diff := cmp.Diff(tt.expected, state); diff != "" {
t.Errorf("unexpected diff (-want +got):\n%s", diff)
}
}
})
}
}
func TestMapFieldsCreate(t *testing.T) {
createdTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", "2025-01-01 00:00:00 +0000 UTC")
if err != nil {
t.Fatalf("failed to parse test time: %v", err)
}
tests := []struct {
description string
input *scf.OrgManagerResponse
expected *Model
isValid bool
}{
{
description: "minimal_input",
input: &scf.OrgManagerResponse{
Guid: utils.Ptr(testUserId),
OrgId: utils.Ptr(testOrgId),
ProjectId: utils.Ptr(testProjectId),
Region: utils.Ptr(testRegion),
CreatedAt: &createdTime,
UpdatedAt: &createdTime,
},
expected: &Model{
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)),
UserId: types.StringValue(testUserId),
OrgId: types.StringValue(testOrgId),
ProjectId: types.StringValue(testProjectId),
Region: types.StringValue(testRegion),
UserName: types.StringNull(),
PlatformId: types.StringNull(),
Password: types.StringNull(),
CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
},
isValid: true,
},
{
description: "max_input",
input: &scf.OrgManagerResponse{
Guid: utils.Ptr(testUserId),
OrgId: utils.Ptr(testOrgId),
ProjectId: utils.Ptr(testProjectId),
PlatformId: utils.Ptr(testPlatformId),
Region: utils.Ptr(testRegion),
CreatedAt: &createdTime,
UpdatedAt: &createdTime,
Username: utils.Ptr("test-user"),
Password: utils.Ptr("test-password"),
},
expected: &Model{
Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)),
UserId: types.StringValue(testUserId),
OrgId: types.StringValue(testOrgId),
ProjectId: types.StringValue(testProjectId),
PlatformId: types.StringValue(testPlatformId),
Region: types.StringValue(testRegion),
UserName: types.StringValue("test-user"),
Password: types.StringValue("test-password"),
CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"),
},
isValid: true,
},
{
description: "nil_org",
input: nil,
expected: nil,
isValid: false,
},
{
description: "empty_org",
input: &scf.OrgManagerResponse{},
expected: nil,
isValid: false,
},
{
description: "missing_id",
input: &scf.OrgManagerResponse{
Username: utils.Ptr("scf-missing-id"),
},
expected: nil,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
state := &Model{}
if tt.expected != nil {
state.ProjectId = tt.expected.ProjectId
}
err := mapFieldsCreate(tt.input, state)
if tt.isValid && err != nil {
t.Fatalf("expected success, got error: %v", err)
}
if !tt.isValid && err == nil {
t.Fatalf("expected error, got nil")
}
if tt.isValid {
if diff := cmp.Diff(tt.expected, state); diff != "" {
t.Errorf("unexpected diff (-want +got):\n%s", diff)
}
}
})
}
}

View file

@ -0,0 +1,219 @@
package platform
import (
"context"
"fmt"
"net/http"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &scfPlatformDataSource{}
_ datasource.DataSourceWithConfigure = &scfPlatformDataSource{}
)
// NewScfPlatformDataSource creates a new instance of the ScfPlatformDataSource.
func NewScfPlatformDataSource() datasource.DataSource {
return &scfPlatformDataSource{}
}
// scfPlatformDataSource is the datasource implementation.
type scfPlatformDataSource struct {
client *scf.APIClient
providerData core.ProviderData
}
func (s *scfPlatformDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) {
var ok bool
s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics)
if !ok {
return
}
apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics)
if response.Diagnostics.HasError() {
return
}
s.client = apiClient
tflog.Info(ctx, "scf client configured for platform")
}
func (s *scfPlatformDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { // nolint:gocritic // function signature required by Terraform
response.TypeName = request.ProviderTypeName + "_scf_platform"
}
type Model struct {
Id types.String `tfsdk:"id"` // Required by Terraform
PlatformId types.String `tfsdk:"platform_id"`
ProjectId types.String `tfsdk:"project_id"`
SystemId types.String `tfsdk:"system_id"`
DisplayName types.String `tfsdk:"display_name"`
Region types.String `tfsdk:"region"`
ApiUrl types.String `tfsdk:"api_url"`
ConsoleUrl types.String `tfsdk:"console_url"`
}
// descriptions for the attributes in the Schema
var descriptions = map[string]string{
"id": "Terraform's internal resource ID, structured as \"`project_id`,`region`,`platform_id`\".",
"platform_id": "The unique id of the platform",
"project_id": "The ID of the project associated with the platform",
"system_id": "The ID of the platform System",
"display_name": "The name of the platform",
"region": "The region where the platform is located. If not defined, the provider region is used",
"api_url": "The CF API Url of the platform",
"console_url": "The Stratos URL of the platform",
}
func (s *scfPlatformDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { // nolint:gocritic // function signature required by Terraform
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
},
"platform_id": schema.StringAttribute{
Description: descriptions["platform_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"system_id": schema.StringAttribute{
Description: descriptions["system_id"],
Computed: true,
},
"display_name": schema.StringAttribute{
Description: descriptions["display_name"],
Computed: true,
},
"region": schema.StringAttribute{
Description: descriptions["region"],
Optional: true,
Computed: true,
},
"api_url": schema.StringAttribute{
Description: descriptions["api_url"],
Computed: true,
},
"console_url": schema.StringAttribute{
Description: descriptions["console_url"],
Computed: true,
},
},
Description: "STACKIT Cloud Foundry Platform datasource schema.",
}
}
func (s *scfPlatformDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve the current state of the resource.
var model Model
diags := request.Config.Get(ctx, &model)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
// Extract the project ID region and platform id of the model
projectId := model.ProjectId.ValueString()
platformId := model.PlatformId.ValueString()
region := s.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "platform_id", platformId)
ctx = tflog.SetField(ctx, "region", region)
// Read the scf platform
scfPlatformResponse, err := s.client.GetPlatformExecute(ctx, projectId, region, platformId)
if err != nil {
utils.LogError(
ctx,
&response.Diagnostics,
err,
"Reading scf platform",
fmt.Sprintf("Platform with ID %q does not exist in project %q.", platformId, projectId),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Platform with ID %q not found or forbidden access", platformId),
},
)
response.State.RemoveResource(ctx)
return
}
err = mapFields(scfPlatformResponse, &model)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf platform", fmt.Sprintf("Processing API response: %v", err))
return
}
// Set the updated state.
diags = response.State.Set(ctx, &model)
response.Diagnostics.Append(diags...)
tflog.Info(ctx, fmt.Sprintf("read scf Platform %s", platformId))
}
// mapFields maps a SCF Platform response to the model.
func mapFields(response *scf.Platforms, model *Model) error {
if response == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var projectId string
if model.ProjectId.ValueString() == "" {
return fmt.Errorf("project id is not present")
}
projectId = model.ProjectId.ValueString()
var region string
if response.Region != nil {
region = *response.Region
} else if model.Region.ValueString() != "" {
region = model.Region.ValueString()
} else {
return fmt.Errorf("region is not present")
}
var platformId string
if response.Guid != nil {
platformId = *response.Guid
} else if model.PlatformId.ValueString() != "" {
platformId = model.PlatformId.ValueString()
} else {
return fmt.Errorf("platform id is not present")
}
// Build the ID
model.Id = utils.BuildInternalTerraformId(projectId, region, platformId)
model.PlatformId = types.StringValue(platformId)
model.ProjectId = types.StringValue(projectId)
model.SystemId = types.StringPointerValue(response.SystemId)
model.DisplayName = types.StringPointerValue(response.DisplayName)
model.Region = types.StringValue(region)
model.ApiUrl = types.StringPointerValue(response.ApiUrl)
model.ConsoleUrl = types.StringPointerValue(response.ConsoleUrl)
return nil
}

View file

@ -0,0 +1,109 @@
package platform
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
)
var (
testProjectId = uuid.New().String()
testPlatformId = uuid.New().String()
testRegion = "eu01"
)
func TestMapFields(t *testing.T) {
tests := []struct {
description string
input *scf.Platforms
expected *Model
isValid bool
}{
{
description: "minimal_input",
input: &scf.Platforms{
Guid: utils.Ptr(testPlatformId),
Region: utils.Ptr(testRegion),
},
expected: &Model{
Id: types.StringValue(fmt.Sprintf("%s,%s,%s", testProjectId, testRegion, testPlatformId)),
PlatformId: types.StringValue(testPlatformId),
ProjectId: types.StringValue(testProjectId),
Region: types.StringValue(testRegion),
SystemId: types.StringNull(),
DisplayName: types.StringNull(),
ApiUrl: types.StringNull(),
ConsoleUrl: types.StringNull(),
},
isValid: true,
},
{
description: "max_input",
input: &scf.Platforms{
Guid: utils.Ptr(testPlatformId),
SystemId: utils.Ptr("eu01.01"),
DisplayName: utils.Ptr("scf-full-org"),
Region: utils.Ptr(testRegion),
ApiUrl: utils.Ptr("https://example.scf.stackit.cloud"),
ConsoleUrl: utils.Ptr("https://example.console.scf.stackit.cloud"),
},
expected: &Model{
Id: types.StringValue(fmt.Sprintf("%s,%s,%s", testProjectId, testRegion, testPlatformId)),
ProjectId: types.StringValue(testProjectId),
PlatformId: types.StringValue(testPlatformId),
Region: types.StringValue(testRegion),
SystemId: types.StringValue("eu01.01"),
DisplayName: types.StringValue("scf-full-org"),
ApiUrl: types.StringValue("https://example.scf.stackit.cloud"),
ConsoleUrl: types.StringValue("https://example.console.scf.stackit.cloud"),
},
isValid: true,
},
{
description: "nil_org",
input: nil,
expected: nil,
isValid: false,
},
{
description: "empty_org",
input: &scf.Platforms{},
expected: nil,
isValid: false,
},
{
description: "missing_id",
input: &scf.Platforms{
DisplayName: utils.Ptr("scf-missing-id"),
},
expected: nil,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
state := &Model{}
if tt.expected != nil {
state.ProjectId = tt.expected.ProjectId
}
err := mapFields(tt.input, state)
if tt.isValid && err != nil {
t.Fatalf("expected success, got error: %v", err)
}
if !tt.isValid && err == nil {
t.Fatalf("expected error, got nil")
}
if tt.isValid {
if diff := cmp.Diff(tt.expected, state); diff != "" {
t.Errorf("unexpected diff (-want +got):\n%s", diff)
}
}
})
}
}

View file

@ -0,0 +1,456 @@
package scf
import (
"context"
_ "embed"
"fmt"
"maps"
"strings"
"testing"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
)
//go:embed testdata/resource-min.tf
var resourceMin string
//go:embed testdata/resource-max.tf
var resourceMax string
var randName = acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
var nameMin = fmt.Sprintf("scf-min-%s-org", randName)
var nameMinUpdated = fmt.Sprintf("scf-min-%s-upd-org", randName)
var nameMax = fmt.Sprintf("scf-max-%s-org", randName)
var nameMaxUpdated = fmt.Sprintf("scf-max-%s-upd-org", randName)
const (
platformName = "Shared Cloud Foundry (public)"
platformSystemId = "01.cf.eu01"
platformIdMax = "0a3d1188-353a-4004-832c-53039c0e3868"
platformApiUrl = "https://api.system.01.cf.eu01.stackit.cloud"
platformConsoleUrl = "https://console.apps.01.cf.eu01.stackit.cloud"
quotaIdMax = "e22cfe1a-0318-473f-88db-61d62dc629c0" // small
quotaIdMaxUpdated = "5ea6b9ab-4048-4bd9-8a8a-5dd7fc40745d" // medium
suspendedMax = true
region = "eu01"
)
var testConfigVarsMin = config.Variables{
"project_id": config.StringVariable(testutil.ProjectId),
"name": config.StringVariable(nameMin),
}
var testConfigVarsMax = config.Variables{
"project_id": config.StringVariable(testutil.ProjectId),
"name": config.StringVariable(nameMax),
"platform_id": config.StringVariable(platformIdMax),
"quota_id": config.StringVariable(quotaIdMax),
"suspended": config.BoolVariable(suspendedMax),
"region": config.StringVariable(region),
}
func testScfOrgConfigVarsMinUpdated() config.Variables {
tempConfig := make(config.Variables, len(testConfigVarsMin))
maps.Copy(tempConfig, testConfigVarsMin)
// update scf organization to a new name
tempConfig["name"] = config.StringVariable(nameMinUpdated)
return tempConfig
}
func testScfOrgConfigVarsMaxUpdated() config.Variables {
tempConfig := make(config.Variables, len(testConfigVarsMax))
maps.Copy(tempConfig, testConfigVarsMax)
// update scf organization to a new name, unsuspend it and assign a new quota
tempConfig["name"] = config.StringVariable(nameMaxUpdated)
tempConfig["quota_id"] = config.StringVariable(quotaIdMaxUpdated)
tempConfig["suspended"] = config.BoolVariable(!suspendedMax)
return tempConfig
}
func TestAccScfOrganizationMin(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckScfOrganizationDestroy,
Steps: []resource.TestStep{
// Creation
{
ConfigVariables: testConfigVarsMin,
Config: testutil.ScfProviderConfig() + resourceMin,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])),
resource.TestCheckResourceAttr("stackit_scf_organization.org", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "created_at"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "platform_id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "org_id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "quota_id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "region"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "status"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "suspended"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "updated_at"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "org_id"),
resource.TestCheckResourceAttr("stackit_scf_organization_manager.orgmanager", "platform_id", testutil.ConvertConfigVariable(testConfigVarsMax["platform_id"])),
resource.TestCheckResourceAttr("stackit_scf_organization_manager.orgmanager", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "user_id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "username"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "password"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "created_at"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "updated_at"),
),
},
// Data source
{
ConfigVariables: testConfigVarsMin,
Config: fmt.Sprintf(`
%s
data "stackit_scf_organization" "org" {
project_id = stackit_scf_organization.org.project_id
org_id = stackit_scf_organization.org.org_id
}
data "stackit_scf_organization_manager" "orgmanager" {
org_id = stackit_scf_organization.org.org_id
project_id = stackit_scf_organization.org.project_id
}
`, testutil.ScfProviderConfig()+resourceMin,
),
Check: resource.ComposeAggregateTestCheckFunc(
// Instance
resource.TestCheckResourceAttr("data.stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "project_id",
"data.stackit_scf_organization.org", "project_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "created_at",
"data.stackit_scf_organization.org", "created_at",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "name",
"data.stackit_scf_organization.org", "name",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "platform_id",
"data.stackit_scf_organization.org", "platform_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "org_id",
"data.stackit_scf_organization.org", "org_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "quota_id",
"data.stackit_scf_organization.org", "quota_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "region",
"data.stackit_scf_organization.org", "region",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "status",
"data.stackit_scf_organization.org", "status",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "suspended",
"data.stackit_scf_organization.org", "suspended",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "updated_at",
"data.stackit_scf_organization.org", "updated_at",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "region",
"data.stackit_scf_organization_manager.orgmanager", "region",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "platform_id",
"data.stackit_scf_organization_manager.orgmanager", "platform_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "project_id",
"data.stackit_scf_organization_manager.orgmanager", "project_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "org_id",
"data.stackit_scf_organization_manager.orgmanager", "org_id",
),
resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "user_id"),
resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "username"),
resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "created_at"),
resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "updated_at"),
),
},
// Import
{
ConfigVariables: testConfigVarsMin,
ResourceName: "stackit_scf_organization.org",
ImportStateIdFunc: func(s *terraform.State) (string, error) {
r, ok := s.RootModule().Resources["stackit_scf_organization.org"]
if !ok {
return "", fmt.Errorf("couldn't find resource stackit_scf_organization.org")
}
orgId, ok := r.Primary.Attributes["org_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute org_id")
}
regionInAttributes, ok := r.Primary.Attributes["region"]
if !ok {
return "", fmt.Errorf("couldn't find attribute region")
}
return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, regionInAttributes, orgId), nil
},
ImportState: true,
ImportStateVerify: true,
},
// Update
{
ConfigVariables: testScfOrgConfigVarsMinUpdated(),
Config: testutil.ScfProviderConfig() + resourceMin,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testScfOrgConfigVarsMinUpdated()["project_id"])),
resource.TestCheckResourceAttr("stackit_scf_organization.org", "name", testutil.ConvertConfigVariable(testScfOrgConfigVarsMinUpdated()["name"])),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "created_at"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "platform_id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "org_id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "quota_id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "region"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "suspended"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "updated_at"),
),
},
// Deletion is done by the framework implicitly
},
})
}
func TestAccScfOrgMax(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckScfOrganizationDestroy,
Steps: []resource.TestStep{
// Creation
{
ConfigVariables: testConfigVarsMax,
Config: testutil.ScfProviderConfig() + resourceMax,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
resource.TestCheckResourceAttr("stackit_scf_organization.org", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])),
resource.TestCheckResourceAttr("stackit_scf_organization.org", "platform_id", testutil.ConvertConfigVariable(testConfigVarsMax["platform_id"])),
resource.TestCheckResourceAttr("stackit_scf_organization.org", "quota_id", testutil.ConvertConfigVariable(testConfigVarsMax["quota_id"])),
resource.TestCheckResourceAttr("stackit_scf_organization.org", "suspended", testutil.ConvertConfigVariable(testConfigVarsMax["suspended"])),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "created_at"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "org_id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "region"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "updated_at"),
resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "platform_id", testutil.ConvertConfigVariable(testConfigVarsMax["platform_id"])),
resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "display_name", platformName),
resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "system_id", platformSystemId),
resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "api_url", platformApiUrl),
resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "console_url", platformConsoleUrl),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "org_id"),
resource.TestCheckResourceAttr("stackit_scf_organization_manager.orgmanager", "platform_id", testutil.ConvertConfigVariable(testConfigVarsMax["platform_id"])),
resource.TestCheckResourceAttr("stackit_scf_organization_manager.orgmanager", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "user_id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "username"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "password"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "created_at"),
resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "updated_at"),
),
},
// Data source
{
ConfigVariables: testConfigVarsMax,
Config: fmt.Sprintf(`
%s
data "stackit_scf_organization" "org" {
project_id = stackit_scf_organization.org.project_id
org_id = stackit_scf_organization.org.org_id
region = var.region
}
data "stackit_scf_organization_manager" "orgmanager" {
org_id = stackit_scf_organization.org.org_id
project_id = stackit_scf_organization.org.project_id
}
data "stackit_scf_platform" "platform" {
platform_id = stackit_scf_organization.org.platform_id
project_id = stackit_scf_organization.org.project_id
}
`, testutil.ScfProviderConfig()+resourceMax,
),
Check: resource.ComposeAggregateTestCheckFunc(
// Instance
resource.TestCheckResourceAttr("data.stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "project_id",
"data.stackit_scf_organization.org", "project_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "created_at",
"data.stackit_scf_organization.org", "created_at",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "name",
"data.stackit_scf_organization.org", "name",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "platform_id",
"data.stackit_scf_organization.org", "platform_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "org_id",
"data.stackit_scf_organization.org", "org_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "quota_id",
"data.stackit_scf_organization.org", "quota_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "region",
"data.stackit_scf_organization.org", "region",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "status",
"data.stackit_scf_organization.org", "status",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "suspended",
"data.stackit_scf_organization.org", "suspended",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "updated_at",
"data.stackit_scf_organization.org", "updated_at",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "platform_id",
"data.stackit_scf_platform.platform", "platform_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "project_id",
"data.stackit_scf_platform.platform", "project_id",
),
resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "display_name", platformName),
resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "system_id", platformSystemId),
resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "display_name", platformName),
resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "region", region),
resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "api_url", platformApiUrl),
resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "console_url", platformConsoleUrl),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "region",
"data.stackit_scf_organization_manager.orgmanager", "region",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "platform_id",
"data.stackit_scf_organization_manager.orgmanager", "platform_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "project_id",
"data.stackit_scf_organization_manager.orgmanager", "project_id",
),
resource.TestCheckResourceAttrPair(
"stackit_scf_organization.org", "org_id",
"data.stackit_scf_organization_manager.orgmanager", "org_id",
),
resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "user_id"),
resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "username"),
resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "created_at"),
resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "updated_at"),
),
},
// Import
{
ConfigVariables: testConfigVarsMax,
ResourceName: "stackit_scf_organization.org",
ImportStateIdFunc: func(s *terraform.State) (string, error) {
r, ok := s.RootModule().Resources["stackit_scf_organization.org"]
if !ok {
return "", fmt.Errorf("couldn't find resource stackit_scf_organization.org")
}
orgId, ok := r.Primary.Attributes["org_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute org_id")
}
regionInAttributes, ok := r.Primary.Attributes["region"]
if !ok {
return "", fmt.Errorf("couldn't find attribute region")
}
return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, regionInAttributes, orgId), nil
},
ImportState: true,
ImportStateVerify: true,
},
// Update
{
ConfigVariables: testScfOrgConfigVarsMaxUpdated(),
Config: testutil.ScfProviderConfig() + resourceMax,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])),
resource.TestCheckResourceAttr("stackit_scf_organization.org", "name", testutil.ConvertConfigVariable(testScfOrgConfigVarsMaxUpdated()["name"])),
resource.TestCheckResourceAttr("stackit_scf_organization.org", "platform_id", testutil.ConvertConfigVariable(testConfigVarsMax["platform_id"])),
resource.TestCheckResourceAttr("stackit_scf_organization.org", "quota_id", testutil.ConvertConfigVariable(testScfOrgConfigVarsMaxUpdated()["quota_id"])),
resource.TestCheckResourceAttr("stackit_scf_organization.org", "suspended", testutil.ConvertConfigVariable(testScfOrgConfigVarsMaxUpdated()["suspended"])),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "created_at"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "org_id"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "region"),
resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "updated_at"),
),
},
// Deletion is done by the framework implicitly
},
})
}
func testAccCheckScfOrganizationDestroy(s *terraform.State) error {
ctx := context.Background()
var client *scf.APIClient
var err error
if testutil.ScfCustomEndpoint == "" {
client, err = scf.NewAPIClient()
} else {
client, err = scf.NewAPIClient(
stackitSdkConfig.WithEndpoint(testutil.ScfCustomEndpoint),
)
}
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
var orgsToDestroy []string
for _, rs := range s.RootModule().Resources {
if rs.Type != "stackit_scf_organization" {
continue
}
orgId := strings.Split(rs.Primary.ID, core.Separator)[1]
orgsToDestroy = append(orgsToDestroy, orgId)
}
organizationsList, err := client.ListOrganizations(ctx, testutil.ProjectId, testutil.Region).Execute()
if err != nil {
return fmt.Errorf("getting scf organizations: %w", err)
}
scfOrgs := organizationsList.GetResources()
for i := range scfOrgs {
if scfOrgs[i].Guid == nil {
continue
}
if utils.Contains(orgsToDestroy, *scfOrgs[i].Guid) {
_, err := client.DeleteOrganizationExecute(ctx, testutil.ProjectId, testutil.Region, *scfOrgs[i].Guid)
if err != nil {
return fmt.Errorf("destroying scf organization %s during CheckDestroy: %w", *scfOrgs[i].Guid, err)
}
}
}
return nil
}

View file

@ -0,0 +1,23 @@
variable "project_id" {}
variable "name" {}
variable "quota_id" {}
variable "suspended" {}
variable "region" {}
resource "stackit_scf_organization" "org" {
project_id = var.project_id
name = var.name
suspended = var.suspended
quota_id = var.quota_id
region = var.region
}
resource "stackit_scf_organization_manager" "orgmanager" {
project_id = var.project_id
org_id = stackit_scf_organization.org.org_id
}
data "stackit_scf_platform" "scf_platform" {
project_id = var.project_id
platform_id = stackit_scf_organization.org.platform_id
}

View file

@ -0,0 +1,13 @@
variable "project_id" {}
variable "name" {}
resource "stackit_scf_organization" "org" {
project_id = var.project_id
name = var.name
}
resource "stackit_scf_organization_manager" "orgmanager" {
project_id = var.project_id
org_id = stackit_scf_organization.org.org_id
}

View file

@ -0,0 +1,30 @@
package utils
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
)
func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *scf.APIClient {
apiClientConfigOptions := []config.ConfigurationOption{
config.WithCustomAuth(providerData.RoundTripper),
utils.UserAgentConfigOption(providerData.Version),
}
if providerData.ScfCustomEndpoint != "" {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ScfCustomEndpoint))
}
apiClient, err := scf.NewAPIClient(apiClientConfigOptions...)
if err != nil {
core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
return nil
}
return apiClient
}

View file

@ -0,0 +1,94 @@
package utils
import (
"context"
"os"
"reflect"
"testing"
"github.com/hashicorp/terraform-plugin-framework/diag"
sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/services/scf"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
)
const (
testVersion = "0.8.15"
testCustomEndpoint = "https://scf-custom-endpoint.api.stackit.cloud"
)
func TestConfigureClient(t *testing.T) {
/* mock authentication by setting service account token env variable */
os.Clearenv()
err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val")
if err != nil {
t.Errorf("error setting env variable: %v", err)
}
type args struct {
providerData *core.ProviderData
}
tests := []struct {
name string
args args
wantErr bool
expected *scf.APIClient
}{
{
name: "default endpoint",
args: args{
providerData: &core.ProviderData{
Version: testVersion,
},
},
expected: func() *scf.APIClient {
apiClient, err := scf.NewAPIClient(
utils.UserAgentConfigOption(testVersion),
)
if err != nil {
t.Errorf("error configuring client: %v", err)
}
return apiClient
}(),
wantErr: false,
},
{
name: "custom endpoint",
args: args{
providerData: &core.ProviderData{
Version: testVersion,
ScfCustomEndpoint: testCustomEndpoint,
},
},
expected: func() *scf.APIClient {
apiClient, err := scf.NewAPIClient(
utils.UserAgentConfigOption(testVersion),
config.WithEndpoint(testCustomEndpoint),
)
if err != nil {
t.Errorf("error configuring client: %v", err)
}
return apiClient
}(),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
diags := diag.Diagnostics{}
actual := ConfigureClient(ctx, tt.args.providerData, &diags)
if diags.HasError() != tt.wantErr {
t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr)
}
if !reflect.DeepEqual(actual, tt.expected) {
t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected)
}
})
}
}

View file

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/stackitcloud/terraform-provider-stackit/stackit"
)
@ -67,6 +68,7 @@ var (
RabbitMQCustomEndpoint = os.Getenv("TF_ACC_RABBITMQ_CUSTOM_ENDPOINT")
RedisCustomEndpoint = os.Getenv("TF_ACC_REDIS_CUSTOM_ENDPOINT")
ResourceManagerCustomEndpoint = os.Getenv("TF_ACC_RESOURCEMANAGER_CUSTOM_ENDPOINT")
ScfCustomEndpoint = os.Getenv("TF_ACC_SCF_CUSTOM_ENDPOINT")
SecretsManagerCustomEndpoint = os.Getenv("TF_ACC_SECRETSMANAGER_CUSTOM_ENDPOINT")
SQLServerFlexCustomEndpoint = os.Getenv("TF_ACC_SQLSERVERFLEX_CUSTOM_ENDPOINT")
ServerBackupCustomEndpoint = os.Getenv("TF_ACC_SERVER_BACKUP_CUSTOM_ENDPOINT")
@ -495,6 +497,22 @@ func GitProviderConfig() string {
)
}
func ScfProviderConfig() string {
if ScfCustomEndpoint == "" {
return `
provider "stackit" {
default_region = "eu01"
}`
}
return fmt.Sprintf(`
provider "stackit" {
default_region = "eu01"
scf_custom_endpoint = "%s"
}`,
ScfCustomEndpoint,
)
}
func ResourceNameWithDateTime(name string) string {
dateTime := time.Now().Format(time.RFC3339)
// Remove timezone to have a smaller datetime

View file

@ -76,6 +76,9 @@ import (
redisInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/instance"
resourceManagerFolder "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/folder"
resourceManagerProject "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/project"
scfOrganization "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/organization"
scfOrganizationmanager "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/organizationmanager"
scfPlatform "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/platform"
secretsManagerInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/instance"
secretsManagerUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/user"
serverBackupSchedule "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/schedule"
@ -147,6 +150,7 @@ type providerModel struct {
ServerUpdateCustomEndpoint types.String `tfsdk:"server_update_custom_endpoint"`
ServiceAccountCustomEndpoint types.String `tfsdk:"service_account_custom_endpoint"`
ResourceManagerCustomEndpoint types.String `tfsdk:"resourcemanager_custom_endpoint"`
ScfCustomEndpoint types.String `tfsdk:"scf_custom_endpoint"`
TokenCustomEndpoint types.String `tfsdk:"token_custom_endpoint"`
EnableBetaResources types.Bool `tfsdk:"enable_beta_resources"`
ServiceEnablementCustomEndpoint types.String `tfsdk:"service_enablement_custom_endpoint"`
@ -185,6 +189,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro
"server_update_custom_endpoint": "Custom endpoint for the Server Update service",
"service_account_custom_endpoint": "Custom endpoint for the Service Account service",
"resourcemanager_custom_endpoint": "Custom endpoint for the Resource Manager service",
"scf_custom_endpoint": "Custom endpoint for the Cloud Foundry (SCF) service",
"secretsmanager_custom_endpoint": "Custom endpoint for the Secrets Manager service",
"sqlserverflex_custom_endpoint": "Custom endpoint for the SQL Server Flex service",
"ske_custom_endpoint": "Custom endpoint for the Kubernetes Engine (SKE) service",
@ -307,6 +312,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro
Optional: true,
Description: descriptions["redis_custom_endpoint"],
},
"scf_custom_endpoint": schema.StringAttribute{
Optional: true,
Description: descriptions["scf_custom_endpoint"],
},
"resourcemanager_custom_endpoint": schema.StringAttribute{
Optional: true,
Description: descriptions["resourcemanager_custom_endpoint"],
@ -417,6 +426,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest,
setStringField(providerConfig.OpenSearchCustomEndpoint, func(v string) { providerData.OpenSearchCustomEndpoint = v })
setStringField(providerConfig.RedisCustomEndpoint, func(v string) { providerData.RedisCustomEndpoint = v })
setStringField(providerConfig.ResourceManagerCustomEndpoint, func(v string) { providerData.ResourceManagerCustomEndpoint = v })
setStringField(providerConfig.ScfCustomEndpoint, func(v string) { providerData.ScfCustomEndpoint = v })
setStringField(providerConfig.SecretsManagerCustomEndpoint, func(v string) { providerData.SecretsManagerCustomEndpoint = v })
setStringField(providerConfig.SQLServerFlexCustomEndpoint, func(v string) { providerData.SQLServerFlexCustomEndpoint = v })
setStringField(providerConfig.ServiceAccountCustomEndpoint, func(v string) { providerData.ServiceAccountCustomEndpoint = v })
@ -500,6 +510,9 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
redisInstance.NewInstanceDataSource,
redisCredential.NewCredentialDataSource,
resourceManagerProject.NewProjectDataSource,
scfOrganization.NewScfOrganizationDataSource,
scfOrganizationmanager.NewScfOrganizationManagerDataSource,
scfPlatform.NewScfPlatformDataSource,
resourceManagerFolder.NewFolderDataSource,
secretsManagerInstance.NewInstanceDataSource,
secretsManagerUser.NewUserDataSource,
@ -567,6 +580,8 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
redisInstance.NewInstanceResource,
redisCredential.NewCredentialResource,
resourceManagerProject.NewProjectResource,
scfOrganization.NewScfOrganizationResource,
scfOrganizationmanager.NewScfOrganizationManagerResource,
resourceManagerFolder.NewFolderResource,
secretsManagerInstance.NewInstanceResource,
secretsManagerUser.NewUserResource,

View file

@ -0,0 +1,248 @@
# How to Provisioning Cloud Foundry using Terrform
## Objective
This tutorial demonstrates how to provision Cloud Foundry resources by
integrating the STACKIT Terraform provider with the Cloud Foundry Terraform
provider. The STACKIT Terraform provider will create a managed Cloud Foundry
organization and set up a technical "org manager" user with
`organization_manager` permissions. These credentials, along with the Cloud
Foundry API URL (retrieved dynamically from a platform data resource), are
passed to the Cloud Foundry Terraform provider to manage resources within the
new organization.
### Output
This configuration creates a Cloud Foundry organization, mirroring the structure
created via the portal. It sets up three distinct spaces: `dev`, `qa`, and
`prod`. The configuration assigns, a specified user the `organization_manager`
and `organization_user` roles at the organization level, and the
`space_developer` role in each space.
### Scope
This tutorial covers the interaction between the STACKIT Terraform provider and
the Cloud Foundry Terraform provider. It assumes you are familiar with:
- Setting up a STACKIT project and configuring the STACKIT Terraform provider
with a service account (see the general STACKIT documentation for details).
- Basic Terraform concepts, such as variables and locals.
This document does not cover foundational topics or every feature of the Cloud
Foundry Terraform provider.
### Example configuration
The following Terraform configuration provisions a Cloud Foundry organization
and related resources using the STACKIT Terraform provider and the Cloud Foundry
Terraform provider:
```
terraform {
required_providers {
stackit = {
source = "stackitcloud/stackit"
}
cloudfoundry = {
source = "cloudfoundry/cloudfoundry"
}
}
}
variable "project_id" {
type = string
description = "Id of the Project"
}
variable "org_name" {
type = string
description = "Name of the Organization"
}
variable "admin_email" {
type = string
description = "Users who are granted permissions"
}
provider "stackit" {
default_region = "eu01"
}
resource "stackit_scf_organization" "scf_org" {
name = var.org_name
project_id = var.project_id
}
data "stackit_scf_platform" "scf_platform" {
project_id = var.project_id
platform_id = stackit_scf_organization.scf_org.platform_id
}
resource "stackit_scf_organization_manager" "scf_manager" {
project_id = var.project_id
org_id = stackit_scf_organization.scf_org.org_id
}
provider "cloudfoundry" {
api_url = data.stackit_scf_platform.scf_platform.api_url
user = stackit_scf_organization_manager.scf_manager.username
password = stackit_scf_organization_manager.scf_manager.password
}
locals {
spaces = ["dev", "qa", "prod"]
}
resource "cloudfoundry_org_role" "org_user" {
username = var.admin_email
type = "organization_user"
org = stackit_scf_organization.scf_org.org_id
}
resource "cloudfoundry_org_role" "org_manager" {
username = var.admin_email
type = "organization_manager"
org = stackit_scf_organization.scf_org.org_id
}
resource "cloudfoundry_space" "spaces" {
for_each = toset(local.spaces)
name = each.key
org = stackit_scf_organization.scf_org.org_id
}
resource "cloudfoundry_space_role" "space_developer" {
for_each = toset(local.spaces)
username = var.admin_email
type = "space_developer"
depends_on = [ cloudfoundry_org_role.org_user ]
space = cloudfoundry_space.spaces[each.key].id
}
```
## Explanation of configuration
### STACKIT provider configuration
```
provider "stackit" {
default_region = "eu01"
}
```
The STACKIT Cloud Foundry Application Programming Interface (SCF API) is
regionalized. Each region operates independently. Set `default_region` in the
provider configuration, to specify the region for all resources, unless you
override it for individual resources. You must also provide access data for the
relevant STACKIT project for the provider to function.
For more details, see
the:[STACKIT Terraform Provider documentation.](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs)
### stackit_scf_organization.scf_org resource
```
resource "stackit_scf_organization" "scf_org" {
name = var.org_name
project_id = var.project_id
}
```
This resource provisions a Cloud Foundry organization, which acts as the
foundational container in the Cloud Foundry environment. Each Cloud Foundry
provider configuration is scoped to a specific organization. The organizations
name, defined by a variable, must be unique across the platform. The
organization is created within a designated STACKIT project, which requires the
STACKIT provider to be configured with the necessary permissions for that
project.
### stackit_scf_organization_manager.scf_manager resource
```
resource "stackit_scf_organization_manager" "scf_manager" {
project_id = var.project_id
org_id = stackit_scf_organization.scf_org.org_id
}
```
This resource creates a technical user in the Cloud Foundry organization with
the organization_manager permission. The user is linked to the organization and
is automatically deleted when the organization is removed.
### stackit_scf_platform.scf_platform data source
```
data "stackit_scf_platform" "scf_platform" {
project_id = var.project_id
platform_id = stackit_scf_organization.scf_org.platform_id
}
```
This data source retrieves properties of the Cloud Foundry platform where the
organization is provisioned. It does not create resources, but provides
information about the existing platform.
### Cloud Foundry provider configuration
```
provider "cloudfoundry" {
api_url = data.stackit_scf_platform.scf_platform.api_url
user = stackit_scf_organization_manager.scf_manager.username
password = stackit_scf_organization_manager.scf_manager.password
}
```
The Cloud Foundry provider is configured to manage resources in the new
organization. The provider uses the API URL from the `stackit_scf_platform` data
source and authenticates using the credentials of the technical user created by
the `stackit_scf_organization_manager` resource.
For more information, see the:
[Cloud Foundry Terraform Provider documentation.](https://registry.terraform.io/providers/cloudfoundry/cloudfoundry/latest/docs)
## Deploy resources
Follow these steps to initialize your environment and provision Cloud Foundry
resources using Terraform.
### Initialize Terraform
Run the following command to initialize the working directory and download the
required provider plugins:
```
terraform init
```
### Create the organization manager user
Run this command to provision the organization and technical user needed to
initialize the Cloud Foundry Terraform provider. This step is required only
during the initial setup. For later changes, you do not need the -target flag.
```
terraform apply -target stackit_scf_organization_manager.scf_manager
```
### Apply the full configuration
Run this command to provision all resources defined in your Terraform
configuration within the Cloud Foundry organization:
```
terraform apply
```
## Verify the deployment
Verify that your Cloud Foundry resources are provisioned correctly. Use the
following Cloud Foundry CLI commands to check applications, services, and
routes:
- `cf apps`
- `cf services`
- `cf routes`
For more information, see the
[Cloud Foundry documentation](https://docs.cloudfoundry.org/) and the
[Cloud Foundry CLI Reference Guide](https://cli.cloudfoundry.org/).