-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add
prefect_account_member
Datasource (#85)
* rename file * add account memberships client + list method * add account_member datasource * final tweaks * fix * yup
- Loading branch information
1 parent
2b5389f
commit 9510152
Showing
10 changed files
with
357 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
data "prefect_account_member" "marvin" { | ||
email = "[email protected]" | ||
} | ||
data "prefect_workspace" "prd" { | ||
id = "<workspace uuid>" | ||
} | ||
data "prefect_workspace_role" "developer" { | ||
name = "Developer" | ||
} | ||
resource "prefect_workspace_access" "marvin_developer" { | ||
accessor_type = "USER" | ||
accessor_id = prefect_account_member.marvin.user_id | ||
workspace_id = data.prefect_workspace.prd.id | ||
workspace_role_id = data.prefect_workspace_role.developer.id | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,15 @@ | ||
data "prefect_workspace_role" "developer" { | ||
name = "Developer" | ||
name = "Developer" | ||
} | ||
data "prefect_workspace" "prd" { | ||
id = "<workspace uuid>" | ||
id = "<workspace uuid>" | ||
} | ||
resource "prefect_service_account" "bot" { | ||
name = "a-cool-bot" | ||
name = "a-cool-bot" | ||
} | ||
resource "prefect_workspace_access" "bot_access" { | ||
accessor_type = "SERVICE_ACCOUNT" | ||
accessor_id = prefect_service_account.bot.id | ||
workspace_id = data.prefect_workspace.prd.id | ||
workspace_role_id = data.prefect_workspace_role.developer.id | ||
resource "prefect_workspace_access" "bot_developer" { | ||
accessor_type = "SERVICE_ACCOUNT" | ||
accessor_id = prefect_service_account.bot.id | ||
workspace_id = data.prefect_workspace.prd.id | ||
workspace_role_id = data.prefect_workspace_role.developer.id | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package api | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"github.com/google/uuid" | ||
) | ||
|
||
type AccountMembershipsClient interface { | ||
List(ctx context.Context, emails []string) ([]*AccountMembership, error) | ||
} | ||
|
||
type AccountMembership struct { | ||
ID uuid.UUID `json:"id"` | ||
ActorID uuid.UUID `json:"actor_id"` | ||
UserID uuid.UUID `json:"user_id"` | ||
FirstName string `json:"first_name"` | ||
LastName string `json:"last_name"` | ||
Handle string `json:"handle"` | ||
Email string `json:"email"` | ||
AccountRoleName string `json:"account_role_name"` | ||
AccountRoleID uuid.UUID `json:"account_role_id"` | ||
LastLogin *time.Time `json:"last_login"` | ||
} | ||
|
||
// AccountMembershipFilter defines the search filter payload | ||
// when searching for workspace roles by name. | ||
// example request payload: | ||
// {"account_memberships": {"email": {"any_": ["test"]}}}. | ||
type AccountMembershipFilter struct { | ||
AccountMemberships struct { | ||
Email struct { | ||
Any []string `json:"any_"` | ||
} `json:"email,omitempty"` | ||
} `json:"account_memberships"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package client | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/google/uuid" | ||
"github.com/prefecthq/terraform-provider-prefect/internal/api" | ||
) | ||
|
||
var _ = api.AccountMembershipsClient(&AccountMembershipsClient{}) | ||
|
||
type AccountMembershipsClient struct { | ||
hc *http.Client | ||
apiKey string | ||
routePrefix string | ||
} | ||
|
||
// AccountMemberships is a factory that initializes and returns a AccountMembershipsClient. | ||
// | ||
//nolint:ireturn // required to support PrefectClient mocking | ||
func (c *Client) AccountMemberships(accountID uuid.UUID) (api.AccountMembershipsClient, error) { | ||
if accountID == uuid.Nil { | ||
accountID = c.defaultAccountID | ||
} | ||
|
||
return &AccountMembershipsClient{ | ||
hc: c.hc, | ||
apiKey: c.apiKey, | ||
routePrefix: fmt.Sprintf("%s/accounts/%s/account_memberships", c.endpoint, accountID.String()), | ||
}, nil | ||
} | ||
|
||
// List returns a list of account memberships, based on the provided filter. | ||
func (c *AccountMembershipsClient) List(ctx context.Context, emails []string) ([]*api.AccountMembership, error) { | ||
var buf bytes.Buffer | ||
filterQuery := api.AccountMembershipFilter{} | ||
filterQuery.AccountMemberships.Email.Any = emails | ||
|
||
if err := json.NewEncoder(&buf).Encode(&filterQuery); err != nil { | ||
return nil, fmt.Errorf("failed to encode filter payload data: %w", err) | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/filter", c.routePrefix), &buf) | ||
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 { | ||
errorBody, _ := io.ReadAll(resp.Body) | ||
|
||
return nil, fmt.Errorf("status code %s, error=%s", resp.Status, errorBody) | ||
} | ||
|
||
var accountMemberships []*api.AccountMembership | ||
if err := json.NewDecoder(resp.Body).Decode(&accountMemberships); err != nil { | ||
return nil, fmt.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
return accountMemberships, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
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" | ||
"github.com/prefecthq/terraform-provider-prefect/internal/provider/helpers" | ||
) | ||
|
||
// Ensure the implementation satisfies the expected interfaces. | ||
var _ datasource.DataSource = &AccountMemberDataSource{} | ||
var _ datasource.DataSourceWithConfigure = &AccountMemberDataSource{} | ||
|
||
type AccountMemberDataSource struct { | ||
client api.PrefectClient | ||
} | ||
|
||
type AccountMemberDataSourceModel struct { | ||
ID customtypes.UUIDValue `tfsdk:"id"` | ||
ActorID customtypes.UUIDValue `tfsdk:"actor_id"` | ||
UserID customtypes.UUIDValue `tfsdk:"user_id"` | ||
FirstName types.String `tfsdk:"first_name"` | ||
LastName types.String `tfsdk:"last_name"` | ||
Handle types.String `tfsdk:"handle"` | ||
Email types.String `tfsdk:"email"` | ||
AccountRoleID customtypes.UUIDValue `tfsdk:"account_role_id"` | ||
AccountRoleName types.String `tfsdk:"account_role_name"` | ||
|
||
AccountID customtypes.UUIDValue `tfsdk:"account_id"` | ||
} | ||
|
||
// NewAccountMemberDataSource returns a new AccountMemberDataSource. | ||
// | ||
//nolint:ireturn // required by Terraform API | ||
func NewAccountMemberDataSource() datasource.DataSource { | ||
return &AccountMemberDataSource{} | ||
} | ||
|
||
// Metadata returns the data source type name. | ||
func (d *AccountMemberDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { | ||
resp.TypeName = req.ProviderTypeName + "_account_member" | ||
} | ||
|
||
// Schema defines the schema for the data source. | ||
func (d *AccountMemberDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { | ||
resp.Schema = schema.Schema{ | ||
Description: "Data Source representing a Prefect Account Member", | ||
Attributes: map[string]schema.Attribute{ | ||
"id": schema.StringAttribute{ | ||
Computed: true, | ||
CustomType: customtypes.UUIDType{}, | ||
Description: "Account Member UUID", | ||
}, | ||
"actor_id": schema.StringAttribute{ | ||
Computed: true, | ||
CustomType: customtypes.UUIDType{}, | ||
Description: "Actor ID UUID", | ||
}, | ||
"user_id": schema.StringAttribute{ | ||
Computed: true, | ||
CustomType: customtypes.UUIDType{}, | ||
Description: "User ID UUID", | ||
}, | ||
"first_name": schema.StringAttribute{ | ||
Computed: true, | ||
Description: "Member First Name", | ||
}, | ||
"last_name": schema.StringAttribute{ | ||
Computed: true, | ||
Description: "Member Last Name", | ||
}, | ||
"handle": schema.StringAttribute{ | ||
Computed: true, | ||
Description: "Member Handle", | ||
}, | ||
"email": schema.StringAttribute{ | ||
Required: true, | ||
Description: "Member Email", | ||
}, | ||
"account_role_id": schema.StringAttribute{ | ||
Computed: true, | ||
CustomType: customtypes.UUIDType{}, | ||
Description: "Account Role ID UUID", | ||
}, | ||
"account_role_name": schema.StringAttribute{ | ||
Computed: true, | ||
Description: "Member Account Role Name", | ||
}, | ||
"account_id": schema.StringAttribute{ | ||
Optional: true, | ||
CustomType: customtypes.UUIDType{}, | ||
Description: "Account UUID where the Account Member resides", | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
// Configure adds the provider-configured client to the data source. | ||
func (d *AccountMemberDataSource) 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 | ||
} | ||
|
||
// Read refreshes the Terraform state with the latest data. | ||
func (d *AccountMemberDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { | ||
var config AccountMemberDataSourceModel | ||
|
||
// Populate the model from data source configuration and emit diagnostics on error | ||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
client, err := d.client.AccountMemberships(config.AccountID.ValueUUID()) | ||
if err != nil { | ||
resp.Diagnostics.Append(helpers.CreateClientErrorDiagnostic("Account Memberships", err)) | ||
|
||
return | ||
} | ||
|
||
// Fetch an existing Account Member by email | ||
// Here, we'd expect only 1 Member (or none) to be returned | ||
// as we are querying a single Member email, not a list of emails | ||
// workspaceRoles, err := client.List(ctx, []string{model.Name.ValueString()}) | ||
accountMembers, err := client.List(ctx, []string{config.Email.ValueString()}) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
"Error refreshing Account Member state", | ||
fmt.Sprintf("Could not search for Account Members, unexpected error: %s", err.Error()), | ||
) | ||
} | ||
|
||
if len(accountMembers) != 1 { | ||
resp.Diagnostics.AddError( | ||
"Could not find Account Member", | ||
fmt.Sprintf("Could not find Account Member with email %s", config.Email.ValueString()), | ||
) | ||
|
||
return | ||
} | ||
|
||
fetchedAccountMember := accountMembers[0] | ||
|
||
config.ID = customtypes.NewUUIDValue(fetchedAccountMember.ID) | ||
config.ActorID = customtypes.NewUUIDValue(fetchedAccountMember.ActorID) | ||
config.UserID = customtypes.NewUUIDValue(fetchedAccountMember.UserID) | ||
config.FirstName = types.StringValue(fetchedAccountMember.FirstName) | ||
config.LastName = types.StringValue(fetchedAccountMember.LastName) | ||
config.Handle = types.StringValue(fetchedAccountMember.Handle) | ||
config.Email = types.StringValue(fetchedAccountMember.Email) | ||
config.AccountRoleID = customtypes.NewUUIDValue(fetchedAccountMember.AccountRoleID) | ||
config.AccountRoleName = types.StringValue(fetchedAccountMember.AccountRoleName) | ||
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package datasources_test | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform-plugin-testing/helper/resource" | ||
"github.com/prefecthq/terraform-provider-prefect/internal/testutils" | ||
) | ||
|
||
func fixtureAccAccountMember(email string) string { | ||
return fmt.Sprintf(` | ||
data "prefect_account_member" "member" { | ||
email = "%s" | ||
} | ||
`, email) | ||
} | ||
|
||
//nolint:paralleltest // we use the resource.ParallelTest helper instead | ||
func TestAccDatasource_account_member(t *testing.T) { | ||
dataSourceName := "data.prefect_account_member.member" | ||
|
||
resource.ParallelTest(t, resource.TestCase{ | ||
ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories, | ||
PreCheck: func() { testutils.AccTestPreCheck(t) }, | ||
Steps: []resource.TestStep{ | ||
{ | ||
Config: fixtureAccAccountMember("[email protected]"), | ||
Check: resource.ComposeAggregateTestCheckFunc( | ||
resource.TestCheckResourceAttr(dataSourceName, "email", "[email protected]"), | ||
resource.TestCheckResourceAttrSet(dataSourceName, "id"), | ||
resource.TestCheckResourceAttrSet(dataSourceName, "account_role_id"), | ||
resource.TestCheckResourceAttrSet(dataSourceName, "account_role_name"), | ||
resource.TestCheckResourceAttrSet(dataSourceName, "actor_id"), | ||
resource.TestCheckResourceAttrSet(dataSourceName, "first_name"), | ||
resource.TestCheckResourceAttrSet(dataSourceName, "last_name"), | ||
resource.TestCheckResourceAttrSet(dataSourceName, "handle"), | ||
resource.TestCheckResourceAttrSet(dataSourceName, "user_id"), | ||
), | ||
}, | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.