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