diff --git a/internal/provider/device/data_source.go b/internal/provider/device/data_source.go new file mode 100644 index 0000000000000000000000000000000000000000..18aefbb2eb8edf24cc4d847ce5fdfbe301bb6a7f --- /dev/null +++ b/internal/provider/device/data_source.go @@ -0,0 +1,206 @@ +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)...) +} diff --git a/internal/provider/device/data_source_test.go b/internal/provider/device/data_source_test.go new file mode 100644 index 0000000000000000000000000000000000000000..107e7dc90df130cf30c94f5ab621e178e87e75b6 --- /dev/null +++ b/internal/provider/device/data_source_test.go @@ -0,0 +1,44 @@ +package device + +import ( + "fmt" + "testing" + + helper "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +func TestAccProvisionerDataSource(t *testing.T) { + t.Parallel() + device := utils.NewDevice(t) + + config := fmt.Sprintf(` +data "smallstep_device" "test" { + id = %q +}`, device.Id) + + helper.Test(t, helper.TestCase{ + ProtoV6ProviderFactories: providerFactories, + Steps: []helper.TestStep{ + { + Config: config, + Check: helper.ComposeAggregateTestCheckFunc( + helper.TestCheckResourceAttr("data.smallstep_device.test", "id", device.Id), + helper.TestCheckResourceAttr("data.smallstep_device.test", "permanent_identifier", device.PermanentIdentifier), + helper.TestCheckResourceAttr("data.smallstep_device.test", "display_id", utils.Deref(device.DisplayId)), + helper.TestCheckResourceAttr("data.smallstep_device.test", "display_name", utils.Deref(device.DisplayName)), + helper.TestCheckResourceAttr("data.smallstep_device.test", "serial", utils.Deref(device.Serial)), + helper.TestCheckResourceAttr("data.smallstep_device.test", "os", string(utils.Deref(device.Os))), + helper.TestCheckResourceAttr("data.smallstep_device.test", "ownership", string(utils.Deref(device.Ownership))), + helper.TestCheckResourceAttr("data.smallstep_device.test", "user.email", device.User.Email), + helper.TestCheckResourceAttr("data.smallstep_device.test", "tags.#", "1"), + helper.TestCheckResourceAttr("data.smallstep_device.test", "metadata.%", "1"), + helper.TestCheckResourceAttr("data.smallstep_device.test", "high_assurance", "false"), + helper.TestCheckResourceAttr("data.smallstep_device.test", "connected", "false"), + helper.TestCheckNoResourceAttr("data.smallstep_device.test", "enrolled_at"), + helper.TestCheckNoResourceAttr("data.smallstep_device.test", "last_seen"), + ), + }, + }, + }) +} diff --git a/internal/provider/device/resource_test.go b/internal/provider/device/resource_test.go index d913b0d2ea814f64baf6f3ff25c2bdabf7b18d3a..d665a6f89748844ee4fc47301c253cb7ab554c79 100644 --- a/internal/provider/device/resource_test.go +++ b/internal/provider/device/resource_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -17,11 +18,9 @@ var provider = &testprovider.SmallstepTestProvider{ ResourceFactories: []func() resource.Resource{ NewResource, }, - /* - DataSourceFactories: []func() datasource.DataSource{ - NewDataSource, - }, - */ + DataSourceFactories: []func() datasource.DataSource{ + NewDataSource, + }, } var providerFactories = map[string]func() (tfprotov6.ProviderServer, error){ diff --git a/internal/provider/utils/testutils.go b/internal/provider/utils/testutils.go index 0b39783ebf7d1529cb5a44e18ae17e532bc7d8d6..5146e04be29d4a63de10879a043b0e1b122afb16 100644 --- a/internal/provider/utils/testutils.go +++ b/internal/provider/utils/testutils.go @@ -174,3 +174,50 @@ func Slug(t *testing.T) string { require.NoError(t, err) return "tfprovider" + slug } + +func NewDevice(t *testing.T) *v20250101.Device { + t.Helper() + + deviceName, err := randutil.Alphanumeric(12) + require.NoError(t, err) + permanentID, err := randutil.Alphanumeric(12) + require.NoError(t, err) + displayID, err := randutil.Alphanumeric(12) + require.NoError(t, err) + serial, err := randutil.Alphanumeric(12) + require.NoError(t, err) + + req := v20250101.DeviceRequest{ + PermanentIdentifier: permanentID, + DisplayId: Ref(displayID), + DisplayName: Ref(deviceName), + Metadata: &v20250101.DeviceMetadata{ + "k1": "v1", + }, + Tags: Ref([]string{"ubuntu"}), + Os: Ref(v20250101.Linux), + Ownership: Ref(v20250101.User), + Serial: Ref(serial), + User: &v20250101.DeviceUser{ + Email: "employee@example.com", + }, + } + + client, err := SmallstepAPIClientFromEnv() + require.NoError(t, err) + + resp, err := client.PostDevices(context.Background(), &v20250101.PostDevicesParams{}, req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Equal(t, 201, resp.StatusCode, string(body)) + + device := &v20250101.Device{} + err = json.Unmarshal(body, device) + require.NoError(t, err) + + return device +} diff --git a/internal/provider/utils/utils.go b/internal/provider/utils/utils.go index 1f76f57556186b4946fe71a4ff8f2490c75c3920..99e95ad99630cc8d63deadd378816428aefcccd8 100644 --- a/internal/provider/utils/utils.go +++ b/internal/provider/utils/utils.go @@ -31,7 +31,7 @@ type dereferencable interface { // Deref gets the default value for a pointer type. This makes it easier to work // with the generated API client code, which uses pointers for optional fields. -func Deref[T dereferencable](v *T) (r T) { +func Deref[T any](v *T) (r T) { if v != nil { r = *v }