From 53cc63ff0eaf18d1cbae9151e352c2102b4e3054 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Fri, 28 Jun 2024 15:28:43 +0200 Subject: [PATCH 1/3] feat: building block resource --- client/buildingblock.go | 80 ++- docs/resources/buildingblock.md | 136 +++++ .../meshstack_buildingblock/resource.tf | 16 + internal/provider/buildingblock_resource.go | 526 ++++++++++++++---- internal/provider/provider.go | 1 + internal/provider/tenant_resource.go | 1 + 6 files changed, 660 insertions(+), 100 deletions(-) create mode 100644 docs/resources/buildingblock.md create mode 100644 examples/resources/meshstack_buildingblock/resource.tf diff --git a/client/buildingblock.go b/client/buildingblock.go index 5580e7b..8b8a7d6 100644 --- a/client/buildingblock.go +++ b/client/buildingblock.go @@ -1,11 +1,23 @@ package client import ( + "bytes" "encoding/json" - "errors" "fmt" "io" "net/http" + "net/url" +) + +const ( + MESH_BUILDING_BLOCK_IO_TYPE_STRING = "STRING" + MESH_BUILDING_BLOCK_IO_TYPE_INTEGER = "INTEGER" + MESH_BUILDING_BLOCK_IO_TYPE_BOOLEAN = "BOOLEAN" + MESH_BUILDING_BLOCK_IO_TYPE_SINGLE_SELECT = "SINGLE_SELECT" + MESH_BUILDING_BLOCK_IO_TYPE_FILE = "FILE" + MESH_BUILDING_BLOCK_IO_TYPE_LIST = "LIST" + + CONTENT_TYPE_BUILDING_BLOCK = "application/vnd.meshcloud.api.meshbuildingblock.v1.hal+json" ) type MeshBuildingBlock struct { @@ -49,12 +61,25 @@ type MeshBuildingBlockStatus struct { Outputs []MeshBuildingBlockIO `json:"outputs" tfsdk:"outputs"` } -func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingBlock, error) { - if c.ensureValidToken() != nil { - return nil, errors.New(ERROR_AUTHENTICATION_FAILURE) - } +type MeshBuildingBlockCreate struct { + ApiVersion string `json:"apiVersion" tfsdk:"api_version"` + Kind string `json:"kind" tfsdk:"kind"` + Metadata MeshBuildingBlockCreateMetadata `json:"metadata" tfsdk:"metadata"` + Spec MeshBuildingBlockSpec `json:"spec" tfsdk:"spec"` +} + +type MeshBuildingBlockCreateMetadata struct { + DefinitionUuid string `json:"definitionUuid" tfsdk:"definition_uuid"` + DefinitionVersion int64 `json:"definitionVersion" tfsdk:"definition_version"` + TenantIdentifier string `json:"tenantIdentifier" tfsdk:"tenant_identifier"` +} + +func (c *MeshStackProviderClient) urlForBuildingBlock(uuid string) *url.URL { + return c.endpoints.BuildingBlocks.JoinPath(uuid) +} - targetUrl := c.endpoints.BuildingBlocks.JoinPath(uuid) +func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingBlock, error) { + targetUrl := c.urlForBuildingBlock(uuid) req, err := http.NewRequest("GET", targetUrl.String(), nil) if err != nil { return nil, err @@ -88,3 +113,46 @@ func (c *MeshStackProviderClient) ReadBuildingBlock(uuid string) (*MeshBuildingB return &bb, nil } + +func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreate) (*MeshBuildingBlock, 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) + req.Header.Set("Accept", CONTENT_TYPE_BUILDING_BLOCK) + + 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 != 201 { + return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) + } + + var createdBb MeshBuildingBlock + err = json.Unmarshal(data, &createdBb) + if err != nil { + return nil, err + } + + return &createdBb, nil +} + +func (c *MeshStackProviderClient) DeleteBuildingBlock(uuid string) error { + targetUrl := c.urlForBuildingBlock(uuid) + return c.deleteMeshObject(*targetUrl, 202) +} diff --git a/docs/resources/buildingblock.md b/docs/resources/buildingblock.md new file mode 100644 index 0000000..f2ecdcf --- /dev/null +++ b/docs/resources/buildingblock.md @@ -0,0 +1,136 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "meshstack_buildingblock Resource - terraform-provider-meshstack" +subcategory: "" +description: |- + Manage Building Block assignment. +--- + +# meshstack_buildingblock (Resource) + +Manage Building Block assignment. + +## Example Usage + +```terraform +resource "meshstack_buildingblock" "my_buildingblock" { + metadata = { + definition_uuid = "f012248e-dda9-4763-8706-641a35de6c62" + definition_version = 1 + tenant_identifier = "my-workspace.my-project-dev.my-platform.my-location" + } + + spec = { + display_name = "my-buildingblock" + + inputs = { + name = { value_string = "my-name" } + size = { value_int = 16 } + } + } +} +``` + + +## Schema + +### Required + +- `metadata` (Attributes) Building Block metadata. (see [below for nested schema](#nestedatt--metadata)) +- `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`. +- `status` (Attributes) Current Building Block status. (see [below for nested schema](#nestedatt--status)) + + +### Nested Schema for `metadata` + +Required: + +- `definition_uuid` (String) UUID of the Building Block Definition this Building Block is based on. +- `definition_version` (Number) Version number of the Building Block Definition this Building Block is based on +- `tenant_identifier` (String) Full tenant identifier of the tenant this Building Block is assigned to. + +Read-Only: + +- `created_on` (String) Timestamp of Building Block creation. +- `force_purge` (Boolean) Indicates whether an operator has requested purging of this Building Block. +- `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. +- `uuid` (String) UUID which uniquely identifies the Building Block. + + + +### Nested Schema for `spec` + +Required: + +- `display_name` (String) Display name for the Building Block as shown in meshPanel. + +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.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 `status` + +Read-Only: + +- `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/examples/resources/meshstack_buildingblock/resource.tf b/examples/resources/meshstack_buildingblock/resource.tf new file mode 100644 index 0000000..e5713a0 --- /dev/null +++ b/examples/resources/meshstack_buildingblock/resource.tf @@ -0,0 +1,16 @@ +resource "meshstack_buildingblock" "my_buildingblock" { + metadata = { + definition_uuid = "f012248e-dda9-4763-8706-641a35de6c62" + definition_version = 1 + tenant_identifier = "my-workspace.my-project-dev.my-platform.my-location" + } + + spec = { + display_name = "my-buildingblock" + + inputs = { + name = { value_string = "my-name" } + size = { value_int = 16 } + } + } +} diff --git a/internal/provider/buildingblock_resource.go b/internal/provider/buildingblock_resource.go index 6e88fe1..a7dc28d 100644 --- a/internal/provider/buildingblock_resource.go +++ b/internal/provider/buildingblock_resource.go @@ -2,175 +2,513 @@ package provider import ( "context" + "encoding/json" "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/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) -// Ensure provider defined types fully satisfy framework interfaces. -var _ resource.Resource = &BuildingBlockResource{} -var _ resource.ResourceWithImportState = &BuildingBlockResource{} +var ( + _ resource.Resource = &buildingBlockResource{} + _ resource.ResourceWithConfigure = &buildingBlockResource{} +) func NewBuildingBlockResource() resource.Resource { - return &BuildingBlockResource{} + return &buildingBlockResource{} } -type BuildingBlockResource struct { +type buildingBlockResource struct { client *client.MeshStackProviderClient } -type BuildingBlockResourceModel struct { - Uuid types.String `tfsdk:"uuid"` - DefinitionUUid types.String `tfsdk:"definition_uuid"` - DefinitionVersion types.Int64 `tfsdk:"definition_version"` - TenantIdentifier types.String `tfsdk:"tenant_identifier"` - DisplayName types.String `tfsdk:"display_name"` - Inputs types.Map `tfsdk:"inputs"` - Parents types.Set `tfsdk:"parents"` +func (r *buildingBlockResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_buildingblock" } -func (r *BuildingBlockResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_buildingblock" +func (r *buildingBlockResource) 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 *BuildingBlockResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *buildingBlockResource) 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: "Building Block", + MarkdownDescription: "Manage Building Block assignment.", Attributes: map[string]schema.Attribute{ - "uuid": schema.StringAttribute{ + "api_version": schema.StringAttribute{ + MarkdownDescription: "Building block datatype version", Computed: true, - MarkdownDescription: "UUID of the Building Block (generated)", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "definition_uuid": schema.StringAttribute{ - Required: true, - MarkdownDescription: "The UUID of the Building Block definition, that is used for this Building Block.", + Default: stringdefault.StaticString("v1"), + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, - "definition_version": schema.Int64Attribute{ - Required: true, - MarkdownDescription: "Version number of the Building Block definition, that is used for this Building Block.", + + "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()}, }, - "tenant_identifier": schema.StringAttribute{ + + "metadata": schema.SingleNestedAttribute{ + MarkdownDescription: "Building Block metadata.", Required: true, - MarkdownDescription: "The identifier of the tenant, this Building Block belongs to.", + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + MarkdownDescription: "UUID which uniquely identifies the Building Block.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "definition_uuid": schema.StringAttribute{ + MarkdownDescription: "UUID of the Building Block Definition this Building Block is based on.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "definition_version": schema.Int64Attribute{ + MarkdownDescription: "Version number of the Building Block Definition this Building Block is based on", + Required: true, + PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}, + }, + "tenant_identifier": schema.StringAttribute{ + MarkdownDescription: "Full tenant identifier of the tenant this Building Block is assigned to.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "force_purge": schema.BoolAttribute{ + MarkdownDescription: "Indicates whether an operator has requested purging of this Building Block.", + Computed: true, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + "created_on": schema.StringAttribute{ + MarkdownDescription: "Timestamp of Building Block creation.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "marked_for_deletion_on": schema.StringAttribute{ + MarkdownDescription: "For deleted Building Blocks: timestamp of deletion.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "marked_for_deletion_by": schema.StringAttribute{ + MarkdownDescription: "For deleted Building Blocks: user who requested deletion.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + }, }, - "display_name": schema.StringAttribute{ + + "spec": schema.SingleNestedAttribute{ + MarkdownDescription: "Building Block specification.", Required: true, - MarkdownDescription: "The display name of the Building Block.", - }, - "inputs": schema.StringAttribute{ - Optional: true, - MarkdownDescription: "Assigned input values for the Building Block.", + Attributes: map[string]schema.Attribute{ + "display_name": schema.StringAttribute{ + MarkdownDescription: "Display name for the Building Block as shown in meshPanel.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + + // + "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, + }, + }, + }, + }, + }, }, - "parents": schema.SetAttribute{ - Optional: true, - MarkdownDescription: "The Building Blocks, that are parents of this Building Block.", + + "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"}...), + }, + }, + "outputs": outputs, + }, }, }, } } -func (r *BuildingBlockResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - if req.ProviderData == nil { - return +type buildingBlockResourceModel struct { + ApiVersion types.String `tfsdk:"api_version"` + Kind types.String `tfsdk:"kind"` + + Metadata struct { + Uuid types.String `tfsdk:"uuid"` + DefinitionUuid types.String `tfsdk:"definition_uuid"` + DefinitionVersion types.Int64 `tfsdk:"definition_version"` + TenantIdentifier types.String `tfsdk:"tenant_identifier"` + ForcePurge types.Bool `tfsdk:"force_purge"` + CreatedOn types.String `tfsdk:"created_on"` + MarkedForDeletionOn types.String `tfsdk:"marked_for_deletion_on"` + MarkedForDeletionBy types.String `tfsdk:"marked_for_deletion_by"` + } `tfsdk:"metadata"` + + Spec struct { + DisplayName types.String `tfsdk:"display_name"` + ParentBuildingBlocks types.List `tfsdk:"parent_building_blocks"` + Inputs map[string]buildingBlockIoModel `tfsdk:"inputs"` + CombinedInputs types.Map `tfsdk:"combined_inputs"` + } `tfsdk:"spec"` + + Status types.Object `tfsdk:"status"` +} + +type buildingBlockIoModel struct { + ValueString types.String `tfsdk:"value_string"` + ValueSingleSelect types.String `tfsdk:"value_single_select"` + ValueFile types.String `tfsdk:"value_file"` + ValueInt types.Int64 `tfsdk:"value_int"` + ValueBool types.Bool `tfsdk:"value_bool"` + ValueList types.String `tfsdk:"value_list"` +} + +func (io *buildingBlockIoModel) extractIoValue() (interface{}, string) { + if !(io.ValueBool.IsNull() || io.ValueBool.IsUnknown()) { + return io.ValueBool.ValueBool(), client.MESH_BUILDING_BLOCK_IO_TYPE_BOOLEAN + } + if !(io.ValueInt.IsNull() || io.ValueInt.IsUnknown()) { + return io.ValueInt.ValueInt64(), client.MESH_BUILDING_BLOCK_IO_TYPE_INTEGER + } + if !(io.ValueSingleSelect.IsNull() || io.ValueSingleSelect.IsUnknown()) { + return io.ValueSingleSelect.ValueString(), client.MESH_BUILDING_BLOCK_IO_TYPE_SINGLE_SELECT } + if !(io.ValueString.IsNull() || io.ValueString.IsUnknown()) { + return io.ValueString.ValueString(), client.MESH_BUILDING_BLOCK_IO_TYPE_STRING + } + return nil, "No value present." +} - client, ok := req.ProviderData.(*client.MeshStackProviderClient) +func (r *buildingBlockResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan buildingBlockResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - if !ok { + bb := client.MeshBuildingBlockCreate{ + ApiVersion: plan.ApiVersion.ValueString(), + Kind: plan.Kind.ValueString(), + + Metadata: client.MeshBuildingBlockCreateMetadata{ + DefinitionUuid: plan.Metadata.DefinitionUuid.ValueString(), + DefinitionVersion: plan.Metadata.DefinitionVersion.ValueInt64(), + TenantIdentifier: plan.Metadata.TenantIdentifier.ValueString(), + }, + + Spec: client.MeshBuildingBlockSpec{ + DisplayName: plan.Spec.DisplayName.ValueString(), + ParentBuildingBlocks: make([]client.MeshBuildingBlockParent, 0), + }, + } + + // 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.CreateBuildingBlock(&bb) + if err != nil { resp.Diagnostics.AddError( - "Unexpected Resource Configure Type", - fmt.Sprintf("Expected *MeshStackProviderClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + "Error creating building block", + "Could not create building block, unexpected error: "+err.Error(), ) - return } + resp.Diagnostics.Append(setStateFromResponse(&ctx, &resp.State, created)...) - r.client = client + // ensure that user inputs are passed along + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("spec").AtName("inputs"), plan.Spec.Inputs)...) } -func (r *BuildingBlockResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data BuildingBlockResourceModel - - // read from PLAN - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) +func (r *buildingBlockResource) 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 } - // TODO use client to create resource + bb, err := r.client.ReadBuildingBlock(uuid) + if err != nil { + resp.Diagnostics.AddError("Unable to read building block", err.Error()) + } - // save to STATE - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + if bb == nil { + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(setStateFromResponse(&ctx, &resp.State, bb)...) } -func (r *BuildingBlockResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data BuildingBlockResourceModel +func (r *buildingBlockResource) 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.") +} - // read from STATE - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) +func (r *buildingBlockResource) 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 } - bb, err := r.client.ReadBuildingBlock(data.Uuid.ValueString()) + err := r.client.DeleteBuildingBlock(uuid) if err != nil { - resp.Diagnostics.AddError("An error occurred while contacting the meshObjects API.", err.Error()) + resp.Diagnostics.AddError( + "Error deleting building block", + "Could not delete building block, unexpected error: "+err.Error(), + ) return } +} - data.Uuid = basetypes.NewStringValue(bb.Metadata.Uuid) - data.DefinitionUUid = basetypes.NewStringValue(bb.Metadata.DefinitionUuid) - data.DefinitionVersion = basetypes.NewInt64Value(bb.Metadata.DefinitionVersion) - data.TenantIdentifier = basetypes.NewStringValue(bb.Metadata.TenantIdentifier) +// 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 *buildingBlockResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +// resource.ImportStatePassthroughID(ctx, path.Root("metadata").AtName("uuid"), req, resp) +// } - data.DisplayName = basetypes.NewStringValue(bb.Spec.DisplayName) +func toResourceModel(io *client.MeshBuildingBlockIO) (*buildingBlockIoModel, error) { + resourceIo := buildingBlockIoModel{} + foundValue := false - // TODO set data.Inputs - // TODO set data.Parents + if io.Value == nil { + return &resourceIo, nil + } + + switch io.ValueType { + case client.MESH_BUILDING_BLOCK_IO_TYPE_BOOLEAN: + value, ok := io.Value.(bool) + if ok { + resourceIo.ValueBool = types.BoolValue(value) + foundValue = true + } + + case client.MESH_BUILDING_BLOCK_IO_TYPE_INTEGER: + // float because it's an untyped JSON value + value, ok := io.Value.(float64) + if ok { + resourceIo.ValueInt = types.Int64Value(int64(value)) + foundValue = true + } + + case client.MESH_BUILDING_BLOCK_IO_TYPE_SINGLE_SELECT: + value, ok := io.Value.(string) + if ok { + resourceIo.ValueSingleSelect = types.StringValue(value) + foundValue = true + } + + case client.MESH_BUILDING_BLOCK_IO_TYPE_STRING: + value, ok := io.Value.(string) + if ok { + resourceIo.ValueString = types.StringValue(value) + foundValue = true + } + + case client.MESH_BUILDING_BLOCK_IO_TYPE_FILE: + value, ok := io.Value.(string) + if ok { + resourceIo.ValueFile = types.StringValue(value) + foundValue = true + } + + case client.MESH_BUILDING_BLOCK_IO_TYPE_LIST: + value, err := json.Marshal(io.Value) + if err != nil { + return nil, err + } + resourceIo.ValueList = types.StringValue(string(value)) + foundValue = true + + default: + return nil, fmt.Errorf("Input type '%s' is not supported.", io.ValueType) + } - // save to STATE - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + if foundValue { + return &resourceIo, nil + } + + return nil, fmt.Errorf("Input '%s' with value type '%s' does not match actual value.", io.Key, io.ValueType) } -func (r *BuildingBlockResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data BuildingBlockResourceModel +func setStateFromResponse(ctx *context.Context, state *tfsdk.State, bb *client.MeshBuildingBlock) diag.Diagnostics { + diags := make(diag.Diagnostics, 0) - // read from PLAN - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { - return - } + diags.Append(state.SetAttribute(*ctx, path.Root("api_version"), bb.ApiVersion)...) + diags.Append(state.SetAttribute(*ctx, path.Root("kind"), bb.Kind)...) - // TODO use client to update resource + diags.Append(state.SetAttribute(*ctx, path.Root("metadata"), bb.Metadata)...) - // save to STATE - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} + diags.Append(state.SetAttribute(*ctx, path.Root("spec").AtName("display_name"), bb.Spec.DisplayName)...) + diags.Append(state.SetAttribute(*ctx, path.Root("spec").AtName("parent_building_blocks"), bb.Spec.ParentBuildingBlocks)...) -func (r *BuildingBlockResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data BuildingBlockResourceModel + combinedInputs := make(map[string]buildingBlockIoModel) + for _, input := range bb.Spec.Inputs { + value, err := toResourceModel(&input) - // read from STATE - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { - return + 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)...) - // TODO use client to delete resource -} + 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)...) -func (r *BuildingBlockResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("uuid"), req, resp) + return diags } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 00715f4..3fccd4a 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -80,6 +80,7 @@ func (p *MeshStackProvider) Resources(ctx context.Context) []func() resource.Res NewTenantResource, NewProjectUserBindingResource, NewProjectGroupBindingResource, + NewBuildingBlockResource, } } diff --git a/internal/provider/tenant_resource.go b/internal/provider/tenant_resource.go index 3f8e7aa..f62c72b 100644 --- a/internal/provider/tenant_resource.go +++ b/internal/provider/tenant_resource.go @@ -54,6 +54,7 @@ func (r *tenantResource) Configure(_ context.Context, req resource.ConfigureRequ r.client = client } + func (r *tenantResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Single tenant by workspace, project, and platform.", From ec7239b4350e923ea7a9f0d873ce6d076362911a Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 4 Jul 2024 11:38:42 +0200 Subject: [PATCH 2/3] fix: updated status codes --- client/buildingblock.go | 2 +- client/project.go | 2 +- client/tenant.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/buildingblock.go b/client/buildingblock.go index 8b8a7d6..44d1e02 100644 --- a/client/buildingblock.go +++ b/client/buildingblock.go @@ -139,7 +139,7 @@ func (c *MeshStackProviderClient) CreateBuildingBlock(bb *MeshBuildingBlockCreat return nil, err } - if res.StatusCode != 201 { + if res.StatusCode != 200 { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } diff --git a/client/project.go b/client/project.go index eca75ca..9dde431 100644 --- a/client/project.go +++ b/client/project.go @@ -178,7 +178,7 @@ func (c *MeshStackProviderClient) CreateProject(project *MeshProjectCreate) (*Me return nil, err } - if res.StatusCode != 201 { + if res.StatusCode != 200 { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } diff --git a/client/tenant.go b/client/tenant.go index b83c5e6..9a4775a 100644 --- a/client/tenant.go +++ b/client/tenant.go @@ -121,7 +121,7 @@ func (c *MeshStackProviderClient) CreateTenant(tenant *MeshTenantCreate) (*MeshT return nil, err } - if res.StatusCode != 201 { + if res.StatusCode != 200 { return nil, fmt.Errorf("unexpected status code: %d, %s", res.StatusCode, data) } From a51ca06967cab7ced58180a8ea4a1c0ebe8af6e8 Mon Sep 17 00:00:00 2001 From: Henry Dettmer Date: Thu, 4 Jul 2024 11:39:24 +0200 Subject: [PATCH 3/3] refactor: buildingblock data source like resource --- docs/data-sources/buildingblock.md | 46 ++-- .../provider/buildingblock_data_source.go | 218 +++++++++--------- 2 files changed, 133 insertions(+), 131 deletions(-) diff --git a/docs/data-sources/buildingblock.md b/docs/data-sources/buildingblock.md index baeb8f0..84f1dd5 100644 --- a/docs/data-sources/buildingblock.md +++ b/docs/data-sources/buildingblock.md @@ -3,12 +3,12 @@ page_title: "meshstack_buildingblock Data Source - terraform-provider-meshstack" subcategory: "" description: |- - Query a single Building Block by UUID. + Single Building Block by UUID. --- # meshstack_buildingblock (Data Source) -Query a single Building Block by UUID. +Single Building Block by UUID. ## Example Usage @@ -25,11 +25,11 @@ data "meshstack_buildingblock" "example" { ### Required -- `metadata` (Attributes) Building Block metadata. UUID of the target Building Block must be set here. (see [below for nested schema](#nestedatt--metadata)) +- `metadata` (Attributes) Building Block metadata. (see [below for nested schema](#nestedatt--metadata)) ### Read-Only -- `api_version` (String) Building Block datatype version +- `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)) @@ -39,17 +39,17 @@ data "meshstack_buildingblock" "example" { Required: -- `uuid` (String) +- `uuid` (String) UUID which uniquely identifies the Building Block. Read-Only: -- `created_on` (String) -- `definition_uuid` (String) -- `definition_version` (Number) -- `force_purge` (Boolean) -- `marked_for_deletion_by` (String) -- `marked_for_deletion_on` (String) -- `tenant_identifier` (String) +- `created_on` (String) Timestamp of Building Block creation. +- `definition_uuid` (String) UUID of the Building Block Definition this Building Block is based on. +- `definition_version` (Number) Version number of the Building Block Definition this Building Block is based on +- `force_purge` (Boolean) Indicates whether an operator has requested purging of this Building Block. +- `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. +- `tenant_identifier` (String) Full tenant identifier of the tenant this Building Block is assigned to. @@ -57,20 +57,21 @@ Read-Only: Read-Only: -- `display_name` (String) -- `inputs` (Attributes List) List of Building Block inputs. (see [below for nested schema](#nestedatt--spec--inputs)) -- `parent_building_blocks` (Attributes List) (see [below for nested schema](#nestedatt--spec--parent_building_blocks)) +- `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)) ### Nested Schema for `spec.inputs` Read-Only: -- `key` (String) - `value_bool` (Boolean) +- `value_file` (String) - `value_int` (Number) +- `value_list` (String) JSON encoded list of objects. +- `value_single_select` (String) - `value_string` (String) -- `value_type` (String) @@ -78,8 +79,8 @@ Read-Only: Read-Only: -- `buildingblock_uuid` (String) -- `definition_uuid` (String) +- `buildingblock_uuid` (String) UUID of the parent Building Block. +- `definition_uuid` (String) UUID of the parent Building Block definition. @@ -88,7 +89,7 @@ Read-Only: Read-Only: -- `outputs` (Attributes List) List of building block outputs. (see [below for nested schema](#nestedatt--status--outputs)) +- `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`. @@ -96,8 +97,9 @@ Read-Only: Read-Only: -- `key` (String) - `value_bool` (Boolean) +- `value_file` (String) - `value_int` (Number) +- `value_list` (String) JSON encoded list of objects. +- `value_single_select` (String) - `value_string` (String) -- `value_type` (String) diff --git a/internal/provider/buildingblock_data_source.go b/internal/provider/buildingblock_data_source.go index e410bd0..64485aa 100644 --- a/internal/provider/buildingblock_data_source.go +++ b/internal/provider/buildingblock_data_source.go @@ -2,7 +2,6 @@ package provider import ( "context" - "errors" "fmt" "github.com/meshcloud/terraform-provider-meshstack/client" @@ -12,7 +11,6 @@ import ( "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. @@ -34,29 +32,47 @@ func (d *buildingBlockDataSource) Metadata(ctx context.Context, req datasource.M } func (d *buildingBlockDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { - // Dynamic attributes are not supported as nested attributes, we use mutually exclusive fields for each possible value type instead. - mkIoList := func(desc string) schema.ListNestedAttribute { - return schema.ListNestedAttribute{ - Computed: true, - MarkdownDescription: desc, + mkIoMap := func() schema.MapNestedAttribute { + return schema.MapNestedAttribute{ + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "key": schema.StringAttribute{Computed: true}, - "value_string": schema.StringAttribute{Computed: true}, - "value_int": schema.Int64Attribute{Computed: true}, - "value_bool": schema.BoolAttribute{Computed: true}, - "value_type": schema.StringAttribute{Computed: true}, + "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: "Query a single Building Block by UUID.", + MarkdownDescription: "Single Building Block by UUID.", Attributes: map[string]schema.Attribute{ "api_version": schema.StringAttribute{ - MarkdownDescription: "Building Block datatype version", + MarkdownDescription: "Building block datatype version", Computed: true, }, @@ -69,20 +85,40 @@ func (d *buildingBlockDataSource) Schema(ctx context.Context, req datasource.Sch }, "metadata": schema.SingleNestedAttribute{ - MarkdownDescription: "Building Block metadata. UUID of the target Building Block must be set here.", + MarkdownDescription: "Building Block metadata.", Required: true, Attributes: map[string]schema.Attribute{ - "uuid": schema.StringAttribute{Required: true}, - "definition_uuid": schema.StringAttribute{Computed: true}, - "definition_version": schema.Int64Attribute{Computed: true}, - "tenant_identifier": schema.StringAttribute{Computed: true}, - "force_purge": schema.BoolAttribute{Computed: true}, - "created_on": schema.StringAttribute{Computed: true}, + "uuid": schema.StringAttribute{ + MarkdownDescription: "UUID which uniquely identifies the Building Block.", + Required: true, + }, + "definition_uuid": schema.StringAttribute{ + MarkdownDescription: "UUID of the Building Block Definition this Building Block is based on.", + Computed: true, + }, + "definition_version": schema.Int64Attribute{ + MarkdownDescription: "Version number of the Building Block Definition this Building Block is based on", + Computed: true, + }, + "tenant_identifier": schema.StringAttribute{ + MarkdownDescription: "Full tenant identifier of the tenant this Building Block is assigned to.", + Computed: true, + }, + "force_purge": schema.BoolAttribute{ + MarkdownDescription: "Indicates whether an operator has requested purging of this Building Block.", + Computed: true, + }, + "created_on": schema.StringAttribute{ + MarkdownDescription: "Timestamp of Building Block creation.", + Computed: true, + }, "marked_for_deletion_on": schema.StringAttribute{ - Computed: true, + MarkdownDescription: "For deleted Building Blocks: timestamp of deletion.", + Computed: true, }, "marked_for_deletion_by": schema.StringAttribute{ - Computed: true, + MarkdownDescription: "For deleted Building Blocks: user who requested deletion.", + Computed: true, }, }, }, @@ -91,15 +127,27 @@ func (d *buildingBlockDataSource) Schema(ctx context.Context, req datasource.Sch 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, + }, + + // + "inputs": inputs, - "display_name": schema.StringAttribute{Computed: true}, - "inputs": mkIoList("List of Building Block inputs."), "parent_building_blocks": schema.ListNestedAttribute{ - Computed: true, + MarkdownDescription: "List of parent Building Blocks.", + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "buildingblock_uuid": schema.StringAttribute{Computed: true}, - "definition_uuid": schema.StringAttribute{Computed: true}, + "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, + }, }, }, }, @@ -117,7 +165,7 @@ func (d *buildingBlockDataSource) Schema(ctx context.Context, req datasource.Sch stringvalidator.OneOf([]string{"WAITING_FOR_DEPENDENT_INPUT", "WAITING_FOR_OPERATOR_INPUT", "PENDING", "IN_PROGRESS", "SUCCEEDED", "FAILED"}...), }, }, - "outputs": mkIoList("List of building block outputs."), + "outputs": outputs, }, }, }, @@ -144,104 +192,56 @@ func (d *buildingBlockDataSource) Configure(ctx context.Context, req datasource. } func (d *buildingBlockDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - type io struct { - Key types.String `tfsdk:"key"` - ValueString types.String `tfsdk:"value_string"` - ValueInt types.Int64 `tfsdk:"value_int"` - ValueBool types.Bool `tfsdk:"value_bool"` - ValueType types.String `tfsdk:"value_type"` - } - - mkIoList := func(ios *[]client.MeshBuildingBlockIO) (*[]io, error) { - result := make([]io, 0) - var err error - for _, input := range *ios { - var valueString *string - var valueInt *int64 - var valueBool *bool - - // TODO: support input type list - if input.ValueType == "STRING" || input.ValueType == "SINGLE_SELECT" || input.ValueType == "FILE" { - val, ok := input.Value.(string) - if !ok { - err = errors.Join(err, fmt.Errorf("Unexpected type '%s' for key '%s'.", input.ValueType, input.Key)) - continue - } - valueString = &val - } else if input.ValueType == "INTEGER" { - val, ok := input.Value.(float64) - if !ok { - err = errors.Join(err, fmt.Errorf("Unexpected type '%s' for key '%s'.", input.ValueType, input.Key)) - continue - } - valInt := int64(val) - valueInt = &valInt - } else if input.ValueType == "BOOLEAN" { - val, ok := input.Value.(bool) - if !ok { - err = errors.Join(err, fmt.Errorf("Unexpected type '%s' for key '%s'.", input.ValueType, input.Key)) - continue - } - valueBool = &val - } - - if err != nil { - continue - } - - result = append(result, io{ - Key: types.StringValue(input.Key), - ValueString: types.StringPointerValue(valueString), - ValueInt: types.Int64PointerValue(valueInt), - ValueBool: types.BoolPointerValue(valueBool), - ValueType: types.StringValue(input.ValueType), - }) - } - return &result, err - } - - // get UUID for BB we want to query from the request 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.ReadBuildingBlock(uuid) if err != nil { - resp.Diagnostics.AddError("Unable to read buildingblock", err.Error()) + resp.Diagnostics.AddError("Unable to read building block", err.Error()) } if bb == nil { - resp.Diagnostics.AddError("Building block not found", fmt.Sprintf("Can't find building block with identifier '%s'.", uuid)) + resp.State.RemoveResource(ctx) return } - // must set attributes individually to handle dynamic input/output types - resp.State.SetAttribute(ctx, path.Root("api_version"), bb.ApiVersion) - resp.State.SetAttribute(ctx, path.Root("kind"), bb.Kind) - resp.State.SetAttribute(ctx, path.Root("metadata"), bb.Metadata) + 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)...) + + 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("parent_building_blocks"), bb.Spec.ParentBuildingBlocks)...) + + inputs := make(map[string]buildingBlockIoModel) + for _, input := range bb.Spec.Inputs { + value, err := toResourceModel(&input) - resp.State.SetAttribute(ctx, path.Root("spec").AtName("display_name"), bb.Spec.DisplayName) - resp.State.SetAttribute(ctx, path.Root("spec").AtName("parent_building_blocks"), bb.Spec.ParentBuildingBlocks) - if bb.Spec.Inputs != nil { - inputs, err := mkIoList(&bb.Spec.Inputs) if err != nil { - resp.Diagnostics.AddError( - "Error(s) while reading inputs/outputs", - err.Error(), - ) + resp.Diagnostics.AddError("Error processing input", err.Error()) return } - resp.State.SetAttribute(ctx, path.Root("spec").AtName("inputs"), inputs) + + inputs[input.Key] = *value } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("spec").AtName("inputs"), inputs)...) + + resp.Diagnostics.Append(resp.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) - resp.State.SetAttribute(ctx, path.Root("status").AtName("status"), bb.Status.Status) - if bb.Status.Outputs != nil { - outputs, err := mkIoList(&bb.Status.Outputs) if err != nil { - resp.Diagnostics.AddError( - "Error(s) while reading inputs/outputs", - err.Error(), - ) + resp.Diagnostics.AddError("Error processing output", err.Error()) return } - resp.State.SetAttribute(ctx, path.Root("status").AtName("outputs"), outputs) + + outputs[output.Key] = *value } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("status").AtName("outputs"), outputs)...) + }