diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e8c441..2c33bd7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
FEATURES:
- Added `meshstack_workspace` resource.
- Added `meshstack_workspace` data source.
+- Added `meshstack_tenant_v4` resource.
+- Added `meshstack_tenant_v4` data source.
FIXES:
- Allow `value_code` in `meshstack_building_block_v2` and `meshstack_building_block` resources.
diff --git a/client/tenant_v4.go b/client/tenant_v4.go
new file mode 100644
index 0000000..00991b5
--- /dev/null
+++ b/client/tenant_v4.go
@@ -0,0 +1,144 @@
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+)
+
+const CONTENT_TYPE_TENANT_V4 = "application/vnd.meshcloud.api.meshtenant.v4-preview.hal+json"
+
+type MeshTenantV4 struct {
+ ApiVersion string `json:"apiVersion" tfsdk:"api_version"`
+ Kind string `json:"kind" tfsdk:"kind"`
+ Metadata MeshTenantV4Metadata `json:"metadata" tfsdk:"metadata"`
+ Spec MeshTenantV4Spec `json:"spec" tfsdk:"spec"`
+ Status MeshTenantV4Status `json:"status" tfsdk:"status"`
+}
+
+type MeshTenantV4Metadata struct {
+ Uuid string `json:"uuid" tfsdk:"uuid"`
+ OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"`
+ OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"`
+ CreatedOn string `json:"createdOn" tfsdk:"created_on"`
+ MarkedForDeletionOn *string `json:"markedForDeletionOn" tfsdk:"marked_for_deletion_on"`
+ DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"`
+}
+
+type MeshTenantV4Spec struct {
+ PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"`
+ PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"`
+ LandingZoneIdentifier *string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"`
+ Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"`
+}
+
+type MeshTenantV4Status struct {
+ TenantName string `json:"tenantName" tfsdk:"tenant_name"`
+ PlatformTypeIdentifier string `json:"platformTypeIdentifier" tfsdk:"platform_type_identifier"`
+ PlatformWorkspaceIdentifier *string `json:"platformWorkspaceIdentifier" tfsdk:"platform_workspace_identifier"`
+ Tags map[string][]string `json:"tags" tfsdk:"tags"`
+}
+
+type MeshTenantV4Create struct {
+ Metadata MeshTenantV4CreateMetadata `json:"metadata" tfsdk:"metadata"`
+ Spec MeshTenantV4CreateSpec `json:"spec" tfsdk:"spec"`
+}
+
+type MeshTenantV4CreateMetadata struct {
+ OwnedByProject string `json:"ownedByProject" tfsdk:"owned_by_project"`
+ OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"`
+}
+
+type MeshTenantV4CreateSpec struct {
+ PlatformIdentifier string `json:"platformIdentifier" tfsdk:"platform_identifier"`
+ LandingZoneIdentifier *string `json:"landingZoneIdentifier" tfsdk:"landing_zone_identifier"`
+ PlatformTenantId *string `json:"platformTenantId" tfsdk:"platform_tenant_id"`
+ Quotas *[]MeshTenantQuota `json:"quotas" tfsdk:"quotas"`
+}
+
+func (c *MeshStackProviderClient) urlForTenantV4(uuid string) *url.URL {
+ return c.endpoints.Tenants.JoinPath(uuid)
+}
+
+func (c *MeshStackProviderClient) ReadTenantV4(uuid string) (*MeshTenantV4, error) {
+ targetUrl := c.urlForTenantV4(uuid)
+ req, err := http.NewRequest("GET", targetUrl.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Accept", CONTENT_TYPE_TENANT_V4)
+
+ res, err := c.doAuthenticatedRequest(req)
+ if err != nil {
+ return nil, err
+ }
+
+ defer res.Body.Close()
+
+ data, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if res.StatusCode == 404 {
+ return nil, nil
+ }
+
+ if !isSuccessHTTPStatus(res) {
+ return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data)
+ }
+
+ var tenant MeshTenantV4
+ err = json.Unmarshal(data, &tenant)
+ if err != nil {
+ return nil, err
+ }
+
+ return &tenant, nil
+}
+
+func (c *MeshStackProviderClient) CreateTenantV4(tenant *MeshTenantV4Create) (*MeshTenantV4, error) {
+ payload, err := json.Marshal(tenant)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", c.endpoints.Tenants.String(), bytes.NewBuffer(payload))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", CONTENT_TYPE_TENANT_V4)
+ req.Header.Set("Accept", CONTENT_TYPE_TENANT_V4)
+
+ res, err := c.doAuthenticatedRequest(req)
+ if err != nil {
+ return nil, err
+ }
+
+ defer res.Body.Close()
+
+ data, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if !isSuccessHTTPStatus(res) {
+ return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data)
+ }
+
+ var createdTenant MeshTenantV4
+ err = json.Unmarshal(data, &createdTenant)
+ if err != nil {
+ return nil, err
+ }
+
+ return &createdTenant, nil
+}
+
+func (c *MeshStackProviderClient) DeleteTenantV4(uuid string) error {
+ targetUrl := c.urlForTenantV4(uuid)
+ return c.deleteMeshObject(*targetUrl, 202)
+}
diff --git a/docs/data-sources/tenant_v4.md b/docs/data-sources/tenant_v4.md
new file mode 100644
index 0000000..0cce476
--- /dev/null
+++ b/docs/data-sources/tenant_v4.md
@@ -0,0 +1,84 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "meshstack_tenant_v4 Data Source - terraform-provider-meshstack"
+subcategory: ""
+description: |-
+ Fetches details of a single tenant by UUID.
+ ~> Note: This resource is in preview and may change in the near future.
+---
+
+# meshstack_tenant_v4 (Data Source)
+
+Fetches details of a single tenant by UUID.
+
+~> **Note:** This resource is in preview and may change in the near future.
+
+## Example Usage
+
+```terraform
+data "meshstack_tenant_v4" "example" {
+ metadata = {
+ uuid = "00000000-0000-0000-0000-000000000000" # Tenant UUID
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `metadata` (Attributes) Tenant metadata. (see [below for nested schema](#nestedatt--metadata))
+
+### Read-Only
+
+- `api_version` (String) Tenant datatype version
+- `kind` (String) meshObject type, always `meshTenant`.
+- `spec` (Attributes) Tenant specification. (see [below for nested schema](#nestedatt--spec))
+- `status` (Attributes) Tenant status. (see [below for nested schema](#nestedatt--status))
+
+
+### Nested Schema for `metadata`
+
+Required:
+
+- `uuid` (String) UUID of the tenant.
+
+Read-Only:
+
+- `created_on` (String) The date the tenant was created.
+- `deleted_on` (String) If the tenant has been submitted for deletion by a workspace manager, the date is shown here.
+- `marked_for_deletion_on` (String) Date when the tenant was marked for deletion.
+- `owned_by_project` (String) Identifier of the project the tenant belongs to.
+- `owned_by_workspace` (String) Identifier of the workspace the tenant belongs to.
+
+
+
+### Nested Schema for `spec`
+
+Read-Only:
+
+- `landing_zone_identifier` (String) Identifier of landing zone to assign to this tenant.
+- `platform_identifier` (String) Identifier of the target platform.
+- `platform_tenant_id` (String) Platform-specific tenant ID.
+- `quotas` (Attributes List) Set of applied tenant quotas. (see [below for nested schema](#nestedatt--spec--quotas))
+
+
+### Nested Schema for `spec.quotas`
+
+Read-Only:
+
+- `key` (String)
+- `value` (Number)
+
+
+
+
+### Nested Schema for `status`
+
+Read-Only:
+
+- `platform_type_identifier` (String) Identifier of the platform type.
+- `platform_workspace_identifier` (String) Identifier of the platform workspace.
+- `tags` (Map of List of String) Tags assigned to this tenant.
+- `tenant_name` (String) Name of the tenant.
diff --git a/docs/resources/tenant_v4.md b/docs/resources/tenant_v4.md
new file mode 100644
index 0000000..40c73a3
--- /dev/null
+++ b/docs/resources/tenant_v4.md
@@ -0,0 +1,118 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "meshstack_tenant_v4 Resource - terraform-provider-meshstack"
+subcategory: ""
+description: |-
+ Manages a meshTenant with API version 4.
+ ~> Note: This resource is in preview and may change in the near future.
+---
+
+# meshstack_tenant_v4 (Resource)
+
+Manages a `meshTenant` with API version 4.
+
+~> **Note:** This resource is in preview and may change in the near future.
+
+## Example Usage
+
+```terraform
+data "meshstack_project" "example" {
+ metadata = {
+ name = "my-project-identifier"
+ owned_by_workspace = "my-workspace-identifier"
+ }
+}
+
+resource "meshstack_tenant_v4" "example" {
+ metadata = {
+ owned_by_workspace = data.meshstack_project.example.metadata.owned_by_workspace
+ owned_by_project = data.meshstack_project.example.metadata.name
+ }
+
+ spec = {
+ platform_identifier = "my-platform-identifier"
+ landing_zone_identifier = "platform-landing-zone-identifier"
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `metadata` (Attributes) Metadata of the tenant. The `owned_by_workspace` and `owned_by_project` attributes must be set here. (see [below for nested schema](#nestedatt--metadata))
+- `spec` (Attributes) Tenant specification. (see [below for nested schema](#nestedatt--spec))
+
+### Read-Only
+
+- `api_version` (String) API version of the tenant resource.
+- `kind` (String) The kind of the meshObject, always `meshTenant`.
+- `status` (Attributes) Tenant status. (see [below for nested schema](#nestedatt--status))
+
+
+### Nested Schema for `metadata`
+
+Required:
+
+- `owned_by_project` (String) The identifier of the project that the tenant belongs to.
+- `owned_by_workspace` (String) The identifier of the workspace that the tenant belongs to.
+
+Read-Only:
+
+- `created_on` (String) The creation timestamp of the meshTenant (e.g. `2020-12-22T09:37:43Z`).
+- `deleted_on` (String) The deletion timestamp of the tenant (e.g. `2020-12-22T09:37:43Z`).
+- `marked_for_deletion_on` (String) The timestamp when the tenant was marked for deletion (e.g. `2020-12-22T09:37:43Z`).
+- `uuid` (String) The unique identifier (UUID) of the tenant.
+
+
+
+### Nested Schema for `spec`
+
+Required:
+
+- `platform_identifier` (String) Identifier of the target platform.
+
+Optional:
+
+- `landing_zone_identifier` (String) The identifier of the landing zone to assign to this tenant.
+- `platform_tenant_id` (String) The identifier of the tenant on the platform (e.g. GCP project ID or Azure subscription ID). If this is not set, a new tenant will be created. If this is set, an existing tenant will be imported. Otherwise, this field will be empty until a successful replication has run.
+- `quotas` (Attributes Set) Landing zone quota settings will be applied by default but can be changed here. (see [below for nested schema](#nestedatt--spec--quotas))
+
+
+### Nested Schema for `spec.quotas`
+
+Required:
+
+- `key` (String)
+- `value` (Number)
+
+
+
+
+### Nested Schema for `status`
+
+Read-Only:
+
+- `platform_type_identifier` (String) Identifier of the platform type.
+- `platform_workspace_identifier` (String) Some platforms create representations of workspaces, in such cases this will contain the identifier of the workspace on the platform.
+- `quotas` (Attributes Set) The effective quotas applied to the tenant. (see [below for nested schema](#nestedatt--status--quotas))
+- `tags` (Map of List of String) Tags assigned to this tenant.
+- `tenant_name` (String) The full tenant name, a concatenation of the workspace identifier, project identifier and platform identifier.
+
+
+### Nested Schema for `status.quotas`
+
+Read-Only:
+
+- `key` (String)
+- `value` (Number)
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# import via uuid
+terraform import 'meshstack_tenant_v4.example' '00000000-0000-0000-0000-000000000000'
+```
diff --git a/examples/data-sources/meshstack_tenant_v4/data-source.tf b/examples/data-sources/meshstack_tenant_v4/data-source.tf
new file mode 100644
index 0000000..f80f330
--- /dev/null
+++ b/examples/data-sources/meshstack_tenant_v4/data-source.tf
@@ -0,0 +1,5 @@
+data "meshstack_tenant_v4" "example" {
+ metadata = {
+ uuid = "00000000-0000-0000-0000-000000000000" # Tenant UUID
+ }
+}
diff --git a/examples/resources/meshstack_tenant_v4/import.sh b/examples/resources/meshstack_tenant_v4/import.sh
new file mode 100644
index 0000000..923478a
--- /dev/null
+++ b/examples/resources/meshstack_tenant_v4/import.sh
@@ -0,0 +1,2 @@
+# import via uuid
+terraform import 'meshstack_tenant_v4.example' '00000000-0000-0000-0000-000000000000'
diff --git a/examples/resources/meshstack_tenant_v4/resource.tf b/examples/resources/meshstack_tenant_v4/resource.tf
new file mode 100644
index 0000000..a9c2e85
--- /dev/null
+++ b/examples/resources/meshstack_tenant_v4/resource.tf
@@ -0,0 +1,18 @@
+data "meshstack_project" "example" {
+ metadata = {
+ name = "my-project-identifier"
+ owned_by_workspace = "my-workspace-identifier"
+ }
+}
+
+resource "meshstack_tenant_v4" "example" {
+ metadata = {
+ owned_by_workspace = data.meshstack_project.example.metadata.owned_by_workspace
+ owned_by_project = data.meshstack_project.example.metadata.name
+ }
+
+ spec = {
+ platform_identifier = "my-platform-identifier"
+ landing_zone_identifier = "platform-landing-zone-identifier"
+ }
+}
diff --git a/internal/provider/buildingblock_resource.go b/internal/provider/buildingblock_resource.go
index 9989a51..2f0c154 100644
--- a/internal/provider/buildingblock_resource.go
+++ b/internal/provider/buildingblock_resource.go
@@ -366,7 +366,7 @@ func (r *buildingBlockResource) Create(ctx context.Context, req resource.CreateR
)
return
}
- resp.Diagnostics.Append(setStateFromResponse(&ctx, &resp.State, created)...)
+ resp.Diagnostics.Append(r.setStateFromResponse(&ctx, &resp.State, created)...)
// ensure that user inputs are passed along
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("spec").AtName("inputs"), plan.Spec.Inputs)...)
@@ -389,7 +389,7 @@ func (r *buildingBlockResource) Read(ctx context.Context, req resource.ReadReque
return
}
- resp.Diagnostics.Append(setStateFromResponse(&ctx, &resp.State, bb)...)
+ resp.Diagnostics.Append(r.setStateFromResponse(&ctx, &resp.State, bb)...)
}
func (r *buildingBlockResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
@@ -489,7 +489,7 @@ func toResourceModel(io *client.MeshBuildingBlockIO) (*buildingBlockIoModel, err
return nil, fmt.Errorf("Input '%s' with value type '%s' does not match actual value.", io.Key, io.ValueType)
}
-func setStateFromResponse(ctx *context.Context, state *tfsdk.State, bb *client.MeshBuildingBlock) diag.Diagnostics {
+func (r *buildingBlockResource) setStateFromResponse(ctx *context.Context, state *tfsdk.State, bb *client.MeshBuildingBlock) diag.Diagnostics {
diags := make(diag.Diagnostics, 0)
diags.Append(state.SetAttribute(*ctx, path.Root("api_version"), bb.ApiVersion)...)
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index bd46b4e..04e3665 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -105,6 +105,7 @@ func (p *MeshStackProvider) Resources(ctx context.Context) []func() resource.Res
return []func() resource.Resource{
NewProjectResource,
NewTenantResource,
+ NewTenantV4Resource,
NewProjectUserBindingResource,
NewProjectGroupBindingResource,
NewWorkspaceResource,
@@ -126,6 +127,7 @@ func (p *MeshStackProvider) DataSources(ctx context.Context) []func() datasource
NewTenantDataSource,
NewTagDefinitionDataSource,
NewTagDefinitionsDataSource,
+ NewTenantV4DataSource,
}
}
diff --git a/internal/provider/tenant_resource.go b/internal/provider/tenant_resource.go
index f07a05a..6e46615 100644
--- a/internal/provider/tenant_resource.go
+++ b/internal/provider/tenant_resource.go
@@ -3,6 +3,7 @@ package provider
import (
"context"
"fmt"
+ "slices"
"strings"
"github.com/meshcloud/terraform-provider-meshstack/client"
@@ -255,14 +256,12 @@ func (r *tenantResource) Delete(ctx context.Context, req resource.DeleteRequest,
func (r *tenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
identifier := strings.Split(req.ID, ".")
- for _, s := range identifier {
- if s == "" {
- resp.Diagnostics.AddError(
- "Incomplete Import Identifier",
- fmt.Sprintf("Encountered empty import identifier field. Got: %q", req.ID),
- )
- return
- }
+ if slices.Contains(identifier, "") {
+ resp.Diagnostics.AddError(
+ "Incomplete Import Identifier",
+ fmt.Sprintf("Encountered empty import identifier field. Got: %q", req.ID),
+ )
+ return
}
if len(identifier) != 4 {
diff --git a/internal/provider/tenant_v4_data_source.go b/internal/provider/tenant_v4_data_source.go
new file mode 100644
index 0000000..141c9d4
--- /dev/null
+++ b/internal/provider/tenant_v4_data_source.go
@@ -0,0 +1,179 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/meshcloud/terraform-provider-meshstack/client"
+
+ "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/path"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ datasource.DataSource = &tenantV4DataSource{}
+ _ datasource.DataSourceWithConfigure = &tenantV4DataSource{}
+)
+
+func NewTenantV4DataSource() datasource.DataSource {
+ return &tenantV4DataSource{}
+}
+
+type tenantV4DataSource struct {
+ client *client.MeshStackProviderClient
+}
+
+func (d *tenantV4DataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_tenant_v4"
+}
+
+func (d *tenantV4DataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*client.MeshStackProviderClient)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *client.MeshStackProviderClient, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = client
+}
+
+func (d *tenantV4DataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Fetches details of a single tenant by UUID.\n\n~> **Note:** This resource is in preview and may change in the near future.",
+ Attributes: map[string]schema.Attribute{
+ "api_version": schema.StringAttribute{
+ MarkdownDescription: "Tenant datatype version",
+ Computed: true,
+ },
+
+ "kind": schema.StringAttribute{
+ MarkdownDescription: "meshObject type, always `meshTenant`.",
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf([]string{"meshTenant"}...),
+ },
+ },
+
+ "metadata": schema.SingleNestedAttribute{
+ MarkdownDescription: "Tenant metadata.",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "uuid": schema.StringAttribute{
+ MarkdownDescription: "UUID of the tenant.",
+ Required: true,
+ },
+ "owned_by_workspace": schema.StringAttribute{
+ MarkdownDescription: "Identifier of the workspace the tenant belongs to.",
+ Computed: true,
+ },
+ "owned_by_project": schema.StringAttribute{
+ MarkdownDescription: "Identifier of the project the tenant belongs to.",
+ Computed: true,
+ },
+ "marked_for_deletion_on": schema.StringAttribute{
+ MarkdownDescription: "Date when the tenant was marked for deletion.",
+ Computed: true,
+ },
+ "deleted_on": schema.StringAttribute{
+ MarkdownDescription: "If the tenant has been submitted for deletion by a workspace manager, the date is shown here.",
+ Computed: true,
+ },
+ "created_on": schema.StringAttribute{
+ MarkdownDescription: "The date the tenant was created.",
+ Computed: true,
+ },
+ },
+ },
+ "spec": schema.SingleNestedAttribute{
+ MarkdownDescription: "Tenant specification.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "platform_identifier": schema.StringAttribute{
+ MarkdownDescription: "Identifier of the target platform.",
+ Computed: true,
+ },
+ "platform_tenant_id": schema.StringAttribute{
+ MarkdownDescription: "Platform-specific tenant ID.",
+ Computed: true,
+ },
+ "landing_zone_identifier": schema.StringAttribute{
+ MarkdownDescription: "Identifier of landing zone to assign to this tenant.",
+ Computed: true,
+ },
+ "quotas": schema.ListNestedAttribute{
+ MarkdownDescription: "Set of applied tenant quotas.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "key": schema.StringAttribute{Computed: true},
+ "value": schema.Int64Attribute{Computed: true},
+ },
+ },
+ },
+ },
+ },
+ "status": schema.SingleNestedAttribute{
+ MarkdownDescription: "Tenant status.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "tenant_name": schema.StringAttribute{
+ MarkdownDescription: "Name of the tenant.",
+ Computed: true,
+ },
+ "platform_type_identifier": schema.StringAttribute{
+ MarkdownDescription: "Identifier of the platform type.",
+ Computed: true,
+ },
+ "platform_workspace_identifier": schema.StringAttribute{
+ MarkdownDescription: "Identifier of the platform workspace.",
+ Computed: true,
+ },
+ "tags": schema.MapAttribute{
+ MarkdownDescription: "Tags assigned to this tenant.",
+ ElementType: types.ListType{ElemType: types.StringType},
+ Computed: true,
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *tenantV4DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var uuid string
+ resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("metadata").AtName("uuid"), &uuid)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tenant, err := d.client.ReadTenantV4(uuid)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error reading tenant",
+ fmt.Sprintf("Could not read tenant, unexpected error: %s", err.Error()),
+ )
+ return
+ }
+
+ if tenant == nil {
+ resp.Diagnostics.AddError(
+ "Error reading tenant",
+ "Tenant not found",
+ )
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, tenant)...)
+}
diff --git a/internal/provider/tenant_v4_resource.go b/internal/provider/tenant_v4_resource.go
new file mode 100644
index 0000000..2361d0e
--- /dev/null
+++ b/internal/provider/tenant_v4_resource.go
@@ -0,0 +1,352 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/meshcloud/terraform-provider-meshstack/client"
+
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/tfsdk"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var (
+ _ resource.Resource = &tenantV4Resource{}
+ _ resource.ResourceWithConfigure = &tenantV4Resource{}
+ _ resource.ResourceWithImportState = &tenantV4Resource{}
+)
+
+type tenantV4ResourceModel struct {
+ ApiVersion types.String `tfsdk:"api_version"`
+ Kind types.String `tfsdk:"kind"`
+ Metadata tenantV4ResourceMetadataModel `tfsdk:"metadata"`
+ Spec tenantV4ResourceSpecModel `tfsdk:"spec"`
+ Status types.Object `tfsdk:"status"`
+}
+
+type tenantV4ResourceMetadataModel struct {
+ Uuid types.String `tfsdk:"uuid"`
+ OwnedByWorkspace types.String `tfsdk:"owned_by_workspace"`
+ OwnedByProject types.String `tfsdk:"owned_by_project"`
+ CreatedOn types.String `tfsdk:"created_on"`
+ DeletedOn types.String `tfsdk:"deleted_on"`
+ MarkedForDeletionOn types.String `tfsdk:"marked_for_deletion_on"`
+}
+
+type tenantV4ResourceSpecModel struct {
+ PlatformIdentifier types.String `tfsdk:"platform_identifier"`
+ PlatformTenantId types.String `tfsdk:"platform_tenant_id"`
+ LandingZoneIdentifier types.String `tfsdk:"landing_zone_identifier"`
+ Quotas types.Set `tfsdk:"quotas"`
+}
+
+type tenantV4ResourceStatusModel struct {
+ TenantName types.String `tfsdk:"tenant_name"`
+ PlatformTypeIdentifier types.String `tfsdk:"platform_type_identifier"`
+ PlatformWorkspaceIdentifier types.String `tfsdk:"platform_workspace_identifier"`
+ Tags types.Map `tfsdk:"tags"`
+ Quotas types.Set `tfsdk:"quotas"`
+}
+
+func NewTenantV4Resource() resource.Resource {
+ return &tenantV4Resource{}
+}
+
+type tenantV4Resource struct {
+ client *client.MeshStackProviderClient
+}
+
+func (r *tenantV4Resource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_tenant_v4"
+}
+
+func (r *tenantV4Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*client.MeshStackProviderClient)
+
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *MeshStackProviderClient, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+
+ return
+ }
+
+ r.client = client
+}
+
+func (r *tenantV4Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Manages a `meshTenant` with API version 4.\n\n~> **Note:** This resource is in preview and may change in the near future.",
+
+ Attributes: map[string]schema.Attribute{
+ "api_version": schema.StringAttribute{
+ MarkdownDescription: "API version of the tenant resource.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+ },
+
+ "kind": schema.StringAttribute{
+ MarkdownDescription: "The kind of the meshObject, always `meshTenant`.",
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf([]string{"meshTenant"}...),
+ },
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+ },
+
+ "metadata": schema.SingleNestedAttribute{
+ MarkdownDescription: "Metadata of the tenant. The `owned_by_workspace` and `owned_by_project` attributes must be set here.",
+ Required: true,
+ PlanModifiers: []planmodifier.Object{objectplanmodifier.RequiresReplace()},
+ Attributes: map[string]schema.Attribute{
+ "uuid": schema.StringAttribute{
+ MarkdownDescription: "The unique identifier (UUID) of the tenant.",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "owned_by_workspace": schema.StringAttribute{
+ MarkdownDescription: "The identifier of the workspace that the tenant belongs to.",
+ Required: true,
+ },
+ "owned_by_project": schema.StringAttribute{
+ MarkdownDescription: "The identifier of the project that the tenant belongs to.",
+ Required: true,
+ },
+ "created_on": schema.StringAttribute{
+ MarkdownDescription: "The creation timestamp of the meshTenant (e.g. `2020-12-22T09:37:43Z`).",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
+ },
+ "deleted_on": schema.StringAttribute{
+ MarkdownDescription: "The deletion timestamp of the tenant (e.g. `2020-12-22T09:37:43Z`).",
+ Computed: true,
+ },
+ "marked_for_deletion_on": schema.StringAttribute{
+ MarkdownDescription: "The timestamp when the tenant was marked for deletion (e.g. `2020-12-22T09:37:43Z`).",
+ Computed: true,
+ },
+ },
+ },
+
+ "spec": schema.SingleNestedAttribute{
+ MarkdownDescription: "Tenant specification.",
+ Required: true,
+ PlanModifiers: []planmodifier.Object{objectplanmodifier.RequiresReplace()},
+ Attributes: map[string]schema.Attribute{
+ "platform_identifier": schema.StringAttribute{
+ MarkdownDescription: "Identifier of the target platform.",
+ Required: true,
+ },
+ "platform_tenant_id": schema.StringAttribute{
+ MarkdownDescription: "The identifier of the tenant on the platform (e.g. GCP project ID or Azure subscription ID). If this is not set, a new tenant will be created. If this is set, an existing tenant will be imported. Otherwise, this field will be empty until a successful replication has run.",
+ Optional: true,
+ Computed: true,
+ },
+ "landing_zone_identifier": schema.StringAttribute{
+ MarkdownDescription: "The identifier of the landing zone to assign to this tenant.",
+ Optional: true,
+ },
+ "quotas": schema.SetNestedAttribute{
+ MarkdownDescription: "Landing zone quota settings will be applied by default but can be changed here.",
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "key": schema.StringAttribute{Required: true},
+ "value": schema.Int64Attribute{Required: true},
+ },
+ },
+ },
+ },
+ },
+
+ "status": schema.SingleNestedAttribute{
+ MarkdownDescription: "Tenant status.",
+ Computed: true,
+ Attributes: map[string]schema.Attribute{
+ "tenant_name": schema.StringAttribute{
+ MarkdownDescription: "The full tenant name, a concatenation of the workspace identifier, project identifier and platform identifier.",
+ Computed: true,
+ },
+ "platform_type_identifier": schema.StringAttribute{
+ MarkdownDescription: "Identifier of the platform type.",
+ Computed: true,
+ },
+ "platform_workspace_identifier": schema.StringAttribute{
+ MarkdownDescription: "Some platforms create representations of workspaces, in such cases this will contain the identifier of the workspace on the platform.",
+ Computed: true,
+ },
+ "tags": schema.MapAttribute{
+ MarkdownDescription: "Tags assigned to this tenant.",
+ ElementType: types.ListType{ElemType: types.StringType},
+ Computed: true,
+ },
+ "quotas": schema.SetNestedAttribute{
+ MarkdownDescription: "The effective quotas applied to the tenant.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "key": schema.StringAttribute{Computed: true},
+ "value": schema.Int64Attribute{Computed: true},
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (r *tenantV4Resource) setStateFromResponse(ctx context.Context, tenant *client.MeshTenantV4, knownQuotas types.Set, state *tfsdk.State, diags *diag.Diagnostics) {
+ diags.Append(state.SetAttribute(ctx, path.Root("api_version"), tenant.ApiVersion)...)
+ diags.Append(state.SetAttribute(ctx, path.Root("kind"), tenant.Kind)...)
+
+ diags.Append(state.SetAttribute(ctx, path.Root("metadata"), tenant.Metadata)...)
+
+ spec := tenantV4ResourceSpecModel{
+ PlatformIdentifier: types.StringValue(tenant.Spec.PlatformIdentifier),
+ PlatformTenantId: types.StringPointerValue(tenant.Spec.PlatformTenantId),
+ LandingZoneIdentifier: types.StringPointerValue(tenant.Spec.LandingZoneIdentifier),
+ Quotas: knownQuotas,
+ }
+ diags.Append(state.SetAttribute(ctx, path.Root("spec"), spec)...)
+
+ quotaAttributeTypes := map[string]attr.Type{
+ "key": types.StringType,
+ "value": types.Int64Type,
+ }
+ quotasStatus, d := types.SetValueFrom(ctx, types.ObjectType{AttrTypes: quotaAttributeTypes}, tenant.Spec.Quotas)
+ diags.Append(d...)
+
+ tagsValue, d := types.MapValueFrom(ctx, types.ListType{ElemType: types.StringType}, tenant.Status.Tags)
+ diags.Append(d...)
+
+ status := tenantV4ResourceStatusModel{
+ TenantName: types.StringValue(tenant.Status.TenantName),
+ PlatformTypeIdentifier: types.StringValue(tenant.Status.PlatformTypeIdentifier),
+ PlatformWorkspaceIdentifier: types.StringPointerValue(tenant.Status.PlatformWorkspaceIdentifier),
+ Tags: tagsValue,
+ Quotas: quotasStatus,
+ }
+ diags.Append(state.SetAttribute(ctx, path.Root("status"), status)...)
+}
+
+func (r *tenantV4Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan tenantV4ResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+
+ var platformTenantId *string
+ if !plan.Spec.PlatformTenantId.IsNull() && !plan.Spec.PlatformTenantId.IsUnknown() {
+ platformTenantId = plan.Spec.PlatformTenantId.ValueStringPointer()
+ }
+
+ var landingZoneIdentifier *string
+ if !plan.Spec.LandingZoneIdentifier.IsNull() && !plan.Spec.LandingZoneIdentifier.IsUnknown() {
+ landingZoneIdentifier = plan.Spec.LandingZoneIdentifier.ValueStringPointer()
+ }
+
+ var quotas []client.MeshTenantQuota
+ if !plan.Spec.Quotas.IsNull() && !plan.Spec.Quotas.IsUnknown() {
+ resp.Diagnostics.Append(plan.Spec.Quotas.ElementsAs(ctx, "as, false)...)
+ }
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ createRequest := client.MeshTenantV4Create{
+ Metadata: client.MeshTenantV4CreateMetadata{
+ OwnedByProject: plan.Metadata.OwnedByProject.ValueString(),
+ OwnedByWorkspace: plan.Metadata.OwnedByWorkspace.ValueString(),
+ },
+ Spec: client.MeshTenantV4CreateSpec{
+ PlatformIdentifier: plan.Spec.PlatformIdentifier.ValueString(),
+ PlatformTenantId: platformTenantId,
+ LandingZoneIdentifier: landingZoneIdentifier,
+ Quotas: "as,
+ },
+ }
+
+ tenant, err := r.client.CreateTenantV4(&createRequest)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error creating tenant",
+ fmt.Sprintf("Could not create tenant, unexpected error: %s", err.Error()),
+ )
+ return
+ }
+
+ r.setStateFromResponse(ctx, tenant, plan.Spec.Quotas, &resp.State, &resp.Diagnostics)
+}
+
+func (r *tenantV4Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ resp.Diagnostics.AddError("Tenants can't be updated", "Unsupported operation: tenant can't be updated.")
+}
+
+func (r *tenantV4Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var uuid types.String
+ resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("metadata").AtName("uuid"), &uuid)...)
+
+ var quotas types.Set
+ resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("spec").AtName("quotas"), "as)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tenant, err := r.client.ReadTenantV4(uuid.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error reading tenant",
+ fmt.Sprintf("Could not read tenant with uuid %s, unexpected error: %s", uuid.ValueString(), err.Error()),
+ )
+ return
+ }
+
+ if tenant == nil {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ r.setStateFromResponse(ctx, tenant, quotas, &resp.State, &resp.Diagnostics)
+}
+
+func (r *tenantV4Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state tenantV4ResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ uuid := state.Metadata.Uuid.ValueString()
+
+ err := r.client.DeleteTenantV4(uuid)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error deleting tenant",
+ fmt.Sprintf("Could not delete tenant with uuid %s, unexpected error: %s", uuid, err.Error()),
+ )
+ return
+ }
+}
+
+func (r *tenantV4Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("metadata").AtName("uuid"), req, resp)
+}