Skip to content
Snippets Groups Projects
data_source.go 5.6 KiB
Newer Older
package device

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/hashicorp/terraform-plugin-framework/datasource"
	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
	"github.com/hashicorp/terraform-plugin-framework/types"
	v20250101 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20250101"
	"github.com/smallstep/terraform-provider-smallstep/internal/provider/utils"
)

var _ datasource.DataSourceWithConfigure = (*DataSource)(nil)

func NewDataSource() datasource.DataSource {
	return &DataSource{}
}

type DataSource struct {
	client *v20250101.Client
}

func (a *DataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
	resp.TypeName = typeName
}

// Configure adds the Smallstep API client to the data source.
func (ds *DataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
	// Prevent panic if the provider has not been configured.
	if req.ProviderData == nil {
		return
	}

	client, ok := req.ProviderData.(*v20250101.Client)

	if !ok {
		resp.Diagnostics.AddError(
			"Get Smallstep API client from provider",
			fmt.Sprintf("Expected *v20250101.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
		)
		return
	}

	ds.client = client
}

func (ds *DataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
	device, props, err := utils.Describe("device")
	if err != nil {
		resp.Diagnostics.AddError(
			"Parse Smallstep OpenAPI Device Schema",
			err.Error(),
		)
		return
	}

	deviceUser, userProps, err := utils.Describe("deviceUser")
	if err != nil {
		resp.Diagnostics.AddError(
			"Parse Smallstep OpenAPI Device User Schema",
			err.Error(),
		)
		return
	}

	resp.Schema = schema.Schema{
		MarkdownDescription: device,

		Attributes: map[string]schema.Attribute{
			"id": schema.StringAttribute{
				MarkdownDescription: props["id"],
				Required:            true,
			},
			"permanent_identifier": schema.StringAttribute{
				MarkdownDescription: props["permanentIdentifier"],
				Computed:            true,
			},
			"serial": schema.StringAttribute{
				MarkdownDescription: props["serial"],
				Computed:            true,
			},
			"display_name": schema.StringAttribute{
				MarkdownDescription: props["displayName"],
				Computed:            true,
			},
			"display_id": schema.StringAttribute{
				MarkdownDescription: props["displayId"],
				Computed:            true,
			},
			"os": schema.StringAttribute{
				MarkdownDescription: props["os"],
				Computed:            true,
			},
			"ownership": schema.StringAttribute{
				MarkdownDescription: props["ownership"],
				Computed:            true,
			},
			"metadata": schema.MapAttribute{
				MarkdownDescription: props["metadata"],
				Computed:            true,
				ElementType:         types.StringType,
			},
			"tags": schema.SetAttribute{
				MarkdownDescription: props["tags"],
				Computed:            true,
				ElementType:         types.StringType,
			},
			"user": schema.SingleNestedAttribute{
				MarkdownDescription: deviceUser,
				Computed:            true,
				Attributes: map[string]schema.Attribute{
					"display_name": schema.StringAttribute{
						MarkdownDescription: userProps["displayName"],
						Computed:            true,
					},
					"email": schema.StringAttribute{
						MarkdownDescription: userProps["email"],
						Computed:            true,
					},
				},
			},
			"connected": schema.BoolAttribute{
				MarkdownDescription: props["connected"],
				Computed:            true,
			},
			"high_assurance": schema.BoolAttribute{
				MarkdownDescription: props["highAssurance"],
				Computed:            true,
			},
			"enrolled_at": schema.StringAttribute{
				MarkdownDescription: props["enrolledAt"],
				Computed:            true,
			},
			"approved_at": schema.StringAttribute{
				MarkdownDescription: props["approvedAt"],
				Computed:            true,
			},
			"last_seen": schema.StringAttribute{
				MarkdownDescription: props["lastSeen"],
				Computed:            true,
			},
		},
	}
}

func (ds *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
	var config Model

	// Read Terraform configuration data into the model
	resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
	if resp.Diagnostics.HasError() {
		return
	}

	deviceID := config.ID.ValueString()
	if deviceID == "" {
		resp.Diagnostics.AddError(
			"Invalid Device ID",
			"Device ID is required",
		)
		return
	}

	httpResp, err := ds.client.GetDevice(ctx, deviceID, &v20250101.GetDeviceParams{})
	if err != nil {
		resp.Diagnostics.AddError(
			"Smallstep API Client Error",
			fmt.Sprintf("Failed to read device %q: %v", config.ID.ValueString(), err),
		)
		return
	}
	defer httpResp.Body.Close()

	if httpResp.StatusCode == http.StatusNotFound {
		resp.State.RemoveResource(ctx)
		return
	}
	if httpResp.StatusCode != http.StatusOK {
		reqID := httpResp.Header.Get("X-Request-Id")
		resp.Diagnostics.AddError(
			"Smallstep API Response Error",
			fmt.Sprintf("Request %q received status %d reading device %s: %s", reqID, httpResp.StatusCode, deviceID, utils.APIErrorMsg(httpResp.Body)),
		)
		return
	}

	device := &v20250101.Device{}
	if err := json.NewDecoder(httpResp.Body).Decode(device); err != nil {
		resp.Diagnostics.AddError(
			"Smallstep API Client Error",
			fmt.Sprintf("Failed to unmarshal device %s: %v", deviceID, err),
		)
		return
	}

	remote, d := fromAPI(ctx, device, req.Config)
	resp.Diagnostics.Append(d...)
	if resp.Diagnostics.HasError() {
		return
	}

	resp.Diagnostics.Append(resp.State.Set(ctx, &remote)...)
}