From b064d5944104da70221a87a4d8c12b7cb6664255 Mon Sep 17 00:00:00 2001 From: Edward Park Date: Fri, 20 Oct 2023 18:43:58 -0700 Subject: [PATCH] feat: add bot datasource --- internal/api/bots.go | 31 ++++++ internal/api/client.go | 1 + internal/client/bots.go | 57 +++++++++++ internal/provider/datasources/bot.go | 142 +++++++++++++++++++++++++++ internal/provider/provider.go | 10 +- 5 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 internal/api/bots.go create mode 100644 internal/client/bots.go create mode 100644 internal/provider/datasources/bot.go diff --git a/internal/api/bots.go b/internal/api/bots.go new file mode 100644 index 00000000..189220ed --- /dev/null +++ b/internal/api/bots.go @@ -0,0 +1,31 @@ +package api + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +// BotsClient is a client for working with Service Accounts +type BotsClient interface { + Get(ctx context.Context, id uuid.UUID) (*Bot, error) +} + +// BotAPIKey represents the nested API Key +// included in a Service Account response +type BotAPIKey struct { + BaseModel + Name string `json:"name"` + Key *string `json:"key"` + Expiration *time.Time `json:"expiration"` +} + +// Bot is the base representation of a Service Account +type Bot struct { + BaseModel + Name string `json:"name"` + AccountID uuid.UUID `json:"account_id"` + AccountRoleName string `json:"account_role_name"` + APIKey *BotAPIKey `json:"api_key"` +} diff --git a/internal/api/client.go b/internal/api/client.go index a39050e1..019e06d7 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -5,6 +5,7 @@ import "github.com/google/uuid" // PrefectClient returns clients for different aspects of our API. type PrefectClient interface { Accounts() (AccountsClient, error) + Bots() (BotsClient, error) Workspaces(accountID uuid.UUID) (WorkspacesClient, error) WorkPools(accountID uuid.UUID, workspaceID uuid.UUID) (WorkPoolsClient, error) Variables(accountID uuid.UUID, workspaceID uuid.UUID) (VariablesClient, error) diff --git a/internal/client/bots.go b/internal/client/bots.go new file mode 100644 index 00000000..01526f67 --- /dev/null +++ b/internal/client/bots.go @@ -0,0 +1,57 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/google/uuid" + "github.com/prefecthq/terraform-provider-prefect/internal/api" +) + +var _ = api.BotsClient(&BotsClient{}) + +// BotsClient is a client for working with service accounts. +type BotsClient struct { + hc *http.Client + apiKey string + routePrefix string +} + +// Bots returns a BotsClient. +func (c *Client) Bots() (api.BotsClient, error) { + return &BotsClient{ + hc: c.hc, + apiKey: c.apiKey, + routePrefix: fmt.Sprintf("%s/api/accounts/%s/bots", c.endpoint, c.defaultAccountID), + }, nil +} + +// Get a single bot by ID +func (c *BotsClient) Get(ctx context.Context, id uuid.UUID) (*api.Bot, error) { + path := fmt.Sprintf("%s/%s", c.routePrefix, id.String()) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, http.NoBody) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + setDefaultHeaders(req, c.apiKey) + + resp, err := c.hc.Do(req) + if err != nil { + return nil, fmt.Errorf("http error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("status code %s", resp.Status) + } + + var bot api.Bot + if err := json.NewDecoder(resp.Body).Decode(&bot); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &bot, nil +} diff --git a/internal/provider/datasources/bot.go b/internal/provider/datasources/bot.go new file mode 100644 index 00000000..3c70f369 --- /dev/null +++ b/internal/provider/datasources/bot.go @@ -0,0 +1,142 @@ +package datasources + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/prefecthq/terraform-provider-prefect/internal/api" + "github.com/prefecthq/terraform-provider-prefect/internal/provider/customtypes" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &BotDataSource{} + _ datasource.DataSourceWithConfigure = &BotDataSource{} +) + +// BotDataSource contains state for the data source modeling a Prefect Service Account. +type BotDataSource struct { + client api.PrefectClient +} + +// AccountDataSourceModel defines the Terraform data source model. +// the TF data source configuration will be unmarsheled into this struct +// NOTE: the APIKey field is not included in bot fetches and +// is excluded from this datasource model +type BotDataSourceModel struct { + ID customtypes.UUIDValue `tfsdk:"id"` + Created customtypes.TimestampValue `tfsdk:"created"` + Updated customtypes.TimestampValue `tfsdk:"updated"` + + Name types.String `tfsdk:"name"` + AccountID customtypes.UUIDValue `tfsdk:"account_id"` + AccountRoleName types.String `tfsdk:"account_role_name"` +} + +func NewBotDataSource() datasource.DataSource { + return &BotDataSource{} +} + +// Metadata returns the data source type name. +func (d *BotDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_bot" +} + +func (d *BotDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Data Source representing a Prefect Service Account", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + CustomType: customtypes.UUIDType{}, + Description: "Service Account UUID", + }, + "created": schema.StringAttribute{ + Computed: true, + CustomType: customtypes.TimestampType{}, + Description: "Date and time of the Service Account creation in RFC 3339 format", + }, + "updated": schema.StringAttribute{ + Computed: true, + CustomType: customtypes.TimestampType{}, + Description: "Date and time that the Service Account was last updated in RFC 3339 format", + }, + "name": schema.StringAttribute{ + Computed: true, + Description: "Name of the Service Account", + }, + "account_id": schema.StringAttribute{ + Computed: true, + CustomType: customtypes.UUIDType{}, + Description: "Account UUID where Service Account resides", + }, + "account_role_name": schema.StringAttribute{ + Computed: true, + Description: "Name of the Service Account", + }, + }, + } +} + +// Configure adds the provider-configured client to the data source. +func (d *BotDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(api.PrefectClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected api.PrefectClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = client +} + +func (d *BotDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var model BotDataSourceModel + + // Populate the model from data source configuration and emit diagnostics on error + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + client, err := d.client.Bots() + if err != nil { + resp.Diagnostics.AddError( + "Error creating the Bots client", + fmt.Sprintf("Could not create Bots client, unexpected error: %s. This is a bug in the provider, please report this to the maintainers.", err.Error()), + ) + return + } + + bot, err := client.Get(ctx, model.ID.ValueUUID()) + if err != nil { + resp.Diagnostics.AddError( + "Failed to fetch Bot and refresh state", + fmt.Sprintf("Could not fetch Bot, unexpected error: %s", err.Error()), + ) + return + } + + model.ID = customtypes.NewUUIDValue(bot.ID) + model.Created = customtypes.NewTimestampPointerValue(bot.Created) + model.Updated = customtypes.NewTimestampPointerValue(bot.Updated) + + model.Name = types.StringValue(bot.Name) + model.AccountID = customtypes.NewUUIDValue(bot.AccountID) + model.AccountRoleName = types.StringValue(bot.AccountRoleName) + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 9eaa05b3..6c8d9196 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -88,15 +88,6 @@ func (p *PrefectProvider) Configure(ctx context.Context, req provider.ConfigureR ) } - if config.APIKey.IsUnknown() { - resp.Diagnostics.AddAttributeError( - path.Root("api_key"), - "Unknown Prefect API Key", - "The Prefect API Key is not known at configuration time. "+ - "Potential resolutions: target apply the source of the value first, set the value statically in the configuration, set the PREFECT_API_URL environment variable, or remove the value.", - ) - } - if config.AccountID.IsUnknown() { resp.Diagnostics.AddAttributeError( path.Root("account_id"), @@ -207,6 +198,7 @@ func (p *PrefectProvider) DataSources(_ context.Context) []func() datasource.Dat datasources.NewWorkPoolDataSource, datasources.NewWorkPoolsDataSource, datasources.NewWorkspaceDataSource, + datasources.NewBotDataSource, } }