diff --git a/CHANGELOG.md b/CHANGELOG.md index 09644e6..7e8c441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## v0.7.2 +## v0.8.0 + +FEATURES: +- Added `meshstack_workspace` resource. +- Added `meshstack_workspace` data source. FIXES: - Allow `value_code` in `meshstack_building_block_v2` and `meshstack_building_block` resources. diff --git a/client/client.go b/client/client.go index c6c629e..7a5e42c 100644 --- a/client/client.go +++ b/client/client.go @@ -37,6 +37,7 @@ type endpoints struct { Projects *url.URL `json:"meshprojects"` ProjectUserBindings *url.URL `json:"meshprojectuserbindings"` ProjectGroupBindings *url.URL `json:"meshprojectgroupbindings"` + Workspaces *url.URL `json:"meshworkspaces"` Tenants *url.URL `json:"meshtenants"` TagDefinitions *url.URL `json:"meshtagdefinitions"` } @@ -63,6 +64,7 @@ func NewClient(rootUrl *url.URL, apiKey string, apiSecret string) (*MeshStackPro Projects: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojects"), ProjectUserBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "userbindings"), ProjectGroupBindings: rootUrl.JoinPath(apiMeshObjectsRoot, "meshprojectbindings", "groupbindings"), + Workspaces: rootUrl.JoinPath(apiMeshObjectsRoot, "meshworkspaces"), Tenants: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtenants"), TagDefinitions: rootUrl.JoinPath(apiMeshObjectsRoot, "meshtagdefinitions"), } diff --git a/client/project.go b/client/project.go index 683b2ee..eca293a 100644 --- a/client/project.go +++ b/client/project.go @@ -67,7 +67,7 @@ func (c *MeshStackProviderClient) ReadProject(workspace string, name string) (*M return nil, err } - if res.StatusCode == 404 { + if res.StatusCode == http.StatusNotFound { return nil, nil } diff --git a/client/workspace.go b/client/workspace.go new file mode 100644 index 0000000..0ade57c --- /dev/null +++ b/client/workspace.go @@ -0,0 +1,160 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +const CONTENT_TYPE_WORKSPACE = "application/vnd.meshcloud.api.meshworkspace.v2.hal+json" + +type MeshWorkspace struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshWorkspaceMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshWorkspaceSpec `json:"spec" tfsdk:"spec"` +} + +type MeshWorkspaceMetadata struct { + Name string `json:"name" tfsdk:"name"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + DeletedOn *string `json:"deletedOn" tfsdk:"deleted_on"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` +} + +type MeshWorkspaceSpec struct { + DisplayName string `json:"displayName" tfsdk:"display_name"` + PlatformBuilderAccessEnabled *bool `json:"platformBuilderAccessEnabled,omitempty" tfsdk:"platform_builder_access_enabled"` +} + +type MeshWorkspaceCreate struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Metadata MeshWorkspaceCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshWorkspaceSpec `json:"spec" tfsdk:"spec"` +} +type MeshWorkspaceCreateMetadata struct { + Name string `json:"name" tfsdk:"name"` + Tags map[string][]string `json:"tags" tfsdk:"tags"` +} + +func (c *MeshStackProviderClient) urlForWorkspace(name string) *url.URL { + return c.endpoints.Workspaces.JoinPath(name) +} + +func (c *MeshStackProviderClient) ReadWorkspace(name string) (*MeshWorkspace, error) { + targetUrl := c.urlForWorkspace(name) + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) + + res, err := c.doAuthenticatedRequest(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + if res.StatusCode == http.StatusNotFound { + return nil, nil // Not found is not an error + } + + 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 workspace MeshWorkspace + err = json.Unmarshal(data, &workspace) + if err != nil { + return nil, err + } + return &workspace, nil +} + +func (c *MeshStackProviderClient) CreateWorkspace(workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { + paylod, err := json.Marshal(workspace) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.Workspaces.String(), bytes.NewBuffer(paylod)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_WORKSPACE) + req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) + + 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 createdWorkspace MeshWorkspace + err = json.Unmarshal(data, &createdWorkspace) + if err != nil { + return nil, err + } + return &createdWorkspace, nil +} + +func (c *MeshStackProviderClient) UpdateWorkspace(name string, workspace *MeshWorkspaceCreate) (*MeshWorkspace, error) { + targetUrl := c.urlForWorkspace(name) + + paylod, err := json.Marshal(workspace) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", targetUrl.String(), bytes.NewBuffer(paylod)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_WORKSPACE) + req.Header.Set("Accept", CONTENT_TYPE_WORKSPACE) + + 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 updatedWorkspace MeshWorkspace + err = json.Unmarshal(data, &updatedWorkspace) + if err != nil { + return nil, err + } + return &updatedWorkspace, nil +} + +func (c *MeshStackProviderClient) DeleteWorkspace(name string) error { + targetUrl := c.urlForWorkspace(name) + return c.deleteMeshObject(*targetUrl, 204) +} diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index 5e49416..66218a7 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -31,7 +31,7 @@ data "meshstack_project" "example" { ### Read-Only - `api_version` (String) Project datatype version -- `kind` (String) meshObject type, always `meshBuildingBlock`. +- `kind` (String) meshObject type, always `meshProject`. - `spec` (Attributes) Project specification. (see [below for nested schema](#nestedatt--spec)) diff --git a/docs/data-sources/workspace.md b/docs/data-sources/workspace.md new file mode 100644 index 0000000..9a665d5 --- /dev/null +++ b/docs/data-sources/workspace.md @@ -0,0 +1,56 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "meshstack_workspace Data Source - terraform-provider-meshstack" +subcategory: "" +description: |- + Read a single workspace by identifier. +--- + +# meshstack_workspace (Data Source) + +Read a single workspace by identifier. + +## Example Usage + +```terraform +data "meshstack_workspace" "example" { + metadata = { + name = "my-workspace-identifier" + } +} +``` + + +## Schema + +### Required + +- `metadata` (Attributes) (see [below for nested schema](#nestedatt--metadata)) + +### Read-Only + +- `api_version` (String) Workspace API version. +- `kind` (String) meshObject type, always `meshWorkspace`. +- `spec` (Attributes) (see [below for nested schema](#nestedatt--spec)) + + +### Nested Schema for `metadata` + +Required: + +- `name` (String) Workspace identifier. + +Read-Only: + +- `created_on` (String) Creation date of the workspace. +- `deleted_on` (String) Deletion date of the workspace. +- `tags` (Map of List of String) Tags of the workspace. + + + +### Nested Schema for `spec` + +Read-Only: + +- `display_name` (String) Display name of the workspace. +- `platform_builder_access_enabled` (Boolean) Whether platform builder access is enabled for the workspace. diff --git a/docs/resources/workspace.md b/docs/resources/workspace.md new file mode 100644 index 0000000..7723622 --- /dev/null +++ b/docs/resources/workspace.md @@ -0,0 +1,82 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "meshstack_workspace Resource - terraform-provider-meshstack" +subcategory: "" +description: |- + Represents a meshStack workspace. + ~> Note: Managing workspaces requires an API key with sufficient admin permissions. +--- + +# meshstack_workspace (Resource) + +Represents a meshStack workspace. + +~> **Note:** Managing workspaces requires an API key with sufficient admin permissions. + +## Example Usage + +```terraform +resource "meshstack_workspace" "example" { + metadata = { + name = "my-workspace-identifier" + tags = { + "cost-center" = [ + "12345" + ] + } + } + spec = { + display_name = "My Workspace's Display Name" + } +} +``` + + +## Schema + +### Required + +- `metadata` (Attributes) (see [below for nested schema](#nestedatt--metadata)) +- `spec` (Attributes) (see [below for nested schema](#nestedatt--spec)) + +### Read-Only + +- `api_version` (String) Workspace datatype version +- `kind` (String) meshObject type, always `meshWorkspace`. + + +### Nested Schema for `metadata` + +Required: + +- `name` (String) Workspace identifier. + +Optional: + +- `tags` (Map of List of String) Tags of the workspace. + +Read-Only: + +- `created_on` (String) Creation date of the workspace. +- `deleted_on` (String) Deletion date of the workspace. + + + +### Nested Schema for `spec` + +Required: + +- `display_name` (String) Display name of the workspace. + +Optional: + +- `platform_builder_access_enabled` (Boolean) Whether platform builder access is enabled for the workspace. + +## Import + +Import is supported using the following syntax: + +```shell +# import via workspace identifier +terraform import meshstack_workspace.example my-workspace-identifier +``` diff --git a/examples/data-sources/meshstack_workspace/data-source.tf b/examples/data-sources/meshstack_workspace/data-source.tf new file mode 100644 index 0000000..18c6dfe --- /dev/null +++ b/examples/data-sources/meshstack_workspace/data-source.tf @@ -0,0 +1,5 @@ +data "meshstack_workspace" "example" { + metadata = { + name = "my-workspace-identifier" + } +} diff --git a/examples/resources/meshstack_workspace/import.sh b/examples/resources/meshstack_workspace/import.sh new file mode 100644 index 0000000..4f545f3 --- /dev/null +++ b/examples/resources/meshstack_workspace/import.sh @@ -0,0 +1,2 @@ +# import via workspace identifier +terraform import meshstack_workspace.example my-workspace-identifier diff --git a/examples/resources/meshstack_workspace/resource.tf b/examples/resources/meshstack_workspace/resource.tf new file mode 100644 index 0000000..007389f --- /dev/null +++ b/examples/resources/meshstack_workspace/resource.tf @@ -0,0 +1,13 @@ +resource "meshstack_workspace" "example" { + metadata = { + name = "my-workspace-identifier" + tags = { + "cost-center" = [ + "12345" + ] + } + } + spec = { + display_name = "My Workspace's Display Name" + } +} diff --git a/internal/provider/project_data_source.go b/internal/provider/project_data_source.go index 7fcb9f2..8b47a58 100644 --- a/internal/provider/project_data_source.go +++ b/internal/provider/project_data_source.go @@ -43,7 +43,7 @@ func (d *projectDataSource) Schema(ctx context.Context, req datasource.SchemaReq }, "kind": schema.StringAttribute{ - MarkdownDescription: "meshObject type, always `meshBuildingBlock`.", + MarkdownDescription: "meshObject type, always `meshProject`.", Computed: true, Validators: []validator.String{ stringvalidator.OneOf([]string{"meshProject"}...), diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 94fe699..bd46b4e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -107,6 +107,7 @@ func (p *MeshStackProvider) Resources(ctx context.Context) []func() resource.Res NewTenantResource, NewProjectUserBindingResource, NewProjectGroupBindingResource, + NewWorkspaceResource, NewBuildingBlockResource, NewBuildingBlockV2Resource, NewTagDefinitionResource, @@ -121,6 +122,7 @@ func (p *MeshStackProvider) DataSources(ctx context.Context) []func() datasource NewProjectsDataSource, NewProjectUserBindingDataSource, NewProjectGroupBindingDataSource, + NewWorkspaceDataSource, NewTenantDataSource, NewTagDefinitionDataSource, NewTagDefinitionsDataSource, diff --git a/internal/provider/workspace_data_source.go b/internal/provider/workspace_data_source.go new file mode 100644 index 0000000..e374e48 --- /dev/null +++ b/internal/provider/workspace_data_source.go @@ -0,0 +1,140 @@ +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" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var ( + _ datasource.DataSource = &workspaceDataSource{} + _ datasource.DataSourceWithConfigure = &workspaceDataSource{} +) + +func NewWorkspaceDataSource() datasource.DataSource { + return &workspaceDataSource{} +} + +type workspaceDataSource struct { + client *client.MeshStackProviderClient +} + +func (d *workspaceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_workspace" +} + +// Schema defines the schema for the data source. +func (d *workspaceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Read a single workspace by identifier.", + + Attributes: map[string]schema.Attribute{ + "api_version": schema.StringAttribute{ + MarkdownDescription: "Workspace API version.", + Computed: true, + }, + "kind": schema.StringAttribute{ + MarkdownDescription: "meshObject type, always `meshWorkspace`.", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf([]string{"meshWorkspace"}...), + }, + }, + + "metadata": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "Workspace identifier.", + Required: true, + }, + "created_on": schema.StringAttribute{ + MarkdownDescription: "Creation date of the workspace.", + Computed: true, + }, + "deleted_on": schema.StringAttribute{ + MarkdownDescription: "Deletion date of the workspace.", + Computed: true, + }, + "tags": schema.MapAttribute{ + MarkdownDescription: "Tags of the workspace.", + ElementType: types.ListType{ElemType: types.StringType}, + Computed: true, + }, + }, + }, + + "spec": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "display_name": schema.StringAttribute{ + MarkdownDescription: "Display name of the workspace.", + Computed: true, + }, + "platform_builder_access_enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether platform builder access is enabled for the workspace.", + Computed: true, + }, + }, + }, + }, + } +} + +// Configure adds the provider configured client to the data source. +func (d *workspaceDataSource) 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 +} + +// Read refreshes the Terraform state with the latest data. +func (d *workspaceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var name string + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("metadata").AtName("name"), &name)...) + + if resp.Diagnostics.HasError() { + return + } + + workspace, err := d.client.ReadWorkspace(name) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Could not read workspace '%s'", name), + err.Error(), + ) + return + } + + if workspace == nil { + resp.Diagnostics.AddError( + "Workspace not found", + fmt.Sprintf("The requested workspace '%s' was not found.", name), + ) + return + } + + // client data maps directly to the schema so we just need to set the state + resp.Diagnostics.Append(resp.State.Set(ctx, workspace)...) +} diff --git a/internal/provider/workspace_resource.go b/internal/provider/workspace_resource.go new file mode 100644 index 0000000..dc93988 --- /dev/null +++ b/internal/provider/workspace_resource.go @@ -0,0 +1,244 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + + "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/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &workspaceResource{} + _ resource.ResourceWithConfigure = &workspaceResource{} + _ resource.ResourceWithImportState = &workspaceResource{} +) + +// NewWorkspaceResource is a helper function to simplify the provider implementation. +func NewWorkspaceResource() resource.Resource { + return &workspaceResource{} +} + +// workspaceResource is the resource implementation. +type workspaceResource struct { + client *client.MeshStackProviderClient +} + +// Metadata returns the resource type name. +func (r *workspaceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_workspace" +} + +// Configure adds the provider configured client to the resource. +func (r *workspaceResource) 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 *workspaceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Represents a meshStack workspace.\n\n~> **Note:** Managing workspaces requires an API key with sufficient admin permissions.", + + Attributes: map[string]schema.Attribute{ + "api_version": schema.StringAttribute{ + MarkdownDescription: "Workspace datatype version", + Computed: true, + Default: stringdefault.StaticString("v2"), + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "kind": schema.StringAttribute{ + MarkdownDescription: "meshObject type, always `meshWorkspace`.", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf([]string{"meshWorkspace"}...), + }, + }, + + "metadata": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "Workspace identifier.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`), + "must be alphanumeric with dashes, must be lowercase, and have no leading, trailing or consecutive dashes", + ), + }, + }, + "created_on": schema.StringAttribute{ + MarkdownDescription: "Creation date of the workspace.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "deleted_on": schema.StringAttribute{ + MarkdownDescription: "Deletion date of the workspace.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "tags": schema.MapAttribute{ + MarkdownDescription: "Tags of the workspace.", + ElementType: types.ListType{ElemType: types.StringType}, + Optional: true, + Computed: true, + Default: mapdefault.StaticValue(types.MapValueMust(types.ListType{ElemType: types.StringType}, map[string]attr.Value{})), + }, + }, + }, + + "spec": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "display_name": schema.StringAttribute{ + MarkdownDescription: "Display name of the workspace.", + Required: true, + }, + "platform_builder_access_enabled": schema.BoolAttribute{ + MarkdownDescription: "Whether platform builder access is enabled for the workspace.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + }, + }, + } +} + +func (r *workspaceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + workspace := client.MeshWorkspaceCreate{ + Metadata: client.MeshWorkspaceCreateMetadata{}, + } + + // Retrieve values from plan + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("api_version"), &workspace.ApiVersion)...) + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("spec"), &workspace.Spec)...) + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("metadata").AtName("name"), &workspace.Metadata.Name)...) + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("metadata").AtName("tags"), &workspace.Metadata.Tags)...) + + if resp.Diagnostics.HasError() { + return + } + + createdWorkspace, err := r.client.CreateWorkspace(&workspace) + if err != nil { + resp.Diagnostics.AddError( + "Error Creating Workspace", + "Could not create workspace, unexpected error: "+err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, createdWorkspace)...) +} + +func (r *workspaceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var name string + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("metadata").AtName("name"), &name)...) + + if resp.Diagnostics.HasError() { + return + } + + workspace, err := r.client.ReadWorkspace(name) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Could not read workspace '%s'", name), + err.Error(), + ) + return + } + + if workspace == nil { + // The workspace was deleted outside of Terraform, so we remove it from the state + resp.State.RemoveResource(ctx) + return + } + + // client data maps directly to the schema so we just need to set the state + resp.Diagnostics.Append(resp.State.Set(ctx, workspace)...) +} + +func (r *workspaceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + workspace := client.MeshWorkspaceCreate{ + Metadata: client.MeshWorkspaceCreateMetadata{}, + } + + // Retrieve values from plan + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("api_version"), &workspace.ApiVersion)...) + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("spec"), &workspace.Spec)...) + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("metadata").AtName("name"), &workspace.Metadata.Name)...) + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("metadata").AtName("tags"), &workspace.Metadata.Tags)...) + + if resp.Diagnostics.HasError() { + return + } + + updatedWorkspace, err := r.client.UpdateWorkspace(workspace.Metadata.Name, &workspace) + if err != nil { + resp.Diagnostics.AddError( + "Error Updating Workspace", + "Could not update workspace, unexpected error: "+err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, updatedWorkspace)...) +} + +func (r *workspaceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var name string + + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("metadata").AtName("name"), &name)...) + + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteWorkspace(name) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Could not delete workspace '%s'", name), + err.Error(), + ) + return + } +} + +func (r *workspaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("metadata").AtName("name"), req, resp) +}