-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat:
prefect_account_role
Datasource (#77)
* add account_roles client * test pre-commit * test pre-commit * test pre-commit * test pre-commit * test pre-commit * add account_roles client * add account_role datasource * remaining * add terraform_fmt * some terraform file formatting * add name validator * add account role lookup * fix tests
- Loading branch information
1 parent
2f6d27f
commit 5199e17
Showing
16 changed files
with
489 additions
and
91 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,17 @@ | ||
--- | ||
fail_fast: false | ||
|
||
repos: | ||
- repo: local | ||
hooks: | ||
- id: golangci-lint | ||
name: golangci-lint | ||
entry: golangci-lint run | ||
types: [go] | ||
language: golang | ||
require_serial: true | ||
pass_filenames: false | ||
- repo: https://github.com/antonbabenko/pre-commit-terraform | ||
rev: v1.83.5 | ||
hooks: | ||
- id: terraform_fmt |
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,7 @@ | ||
data "prefect_account_role" "owner" { | ||
name = "Owner" | ||
} | ||
|
||
data "prefect_account_role" "member" { | ||
name = "Member" | ||
} |
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,3 +1,3 @@ | ||
PREFECT_API_KEY="" | ||
PREFECT_ACCOUNT_ID="" | ||
PREFECT_API_URL="https://api.prefect.cloud/api" | ||
PREFECT_API_KEY = "" | ||
PREFECT_ACCOUNT_ID = "" | ||
PREFECT_API_URL = "https://api.prefect.cloud/api" |
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
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,9 +1,9 @@ | ||
provider "time" {} | ||
resource "time_rotating" "ninety_days" { | ||
rotation_days = 90 | ||
rotation_days = 90 | ||
} | ||
|
||
resource "prefect_service_account" "sa_example" { | ||
name = "my-service-account" | ||
api_key_expiration = time_rotating.ninety_days.rotation_rfc3339 | ||
name = "my-service-account" | ||
api_key_expiration = time_rotating.ninety_days.rotation_rfc3339 | ||
} |
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,34 @@ | ||
package api | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/google/uuid" | ||
) | ||
|
||
type AccountRolesClient interface { | ||
Get(ctx context.Context, roleID uuid.UUID) (*AccountRole, error) | ||
List(ctx context.Context, roleNames []string) ([]*AccountRole, error) | ||
} | ||
|
||
// AccountRole is a representation of an account role. | ||
type AccountRole struct { | ||
BaseModel | ||
Name string `json:"name"` | ||
Permissions []string `json:"permissions"` | ||
|
||
AccountID *uuid.UUID `json:"account_id"` | ||
IsSystemRole bool `json:"is_system_role"` | ||
} | ||
|
||
// AccountRoleFilter defines the search filter payload | ||
// when searching for workspace roles by name. | ||
// example request payload: | ||
// {"account_roles": {"name": {"any_": ["test"]}}}. | ||
type AccountRoleFilter struct { | ||
AccountRoles struct { | ||
Name struct { | ||
Any []string `json:"any_"` | ||
} `json:"name"` | ||
} `json:"account_roles"` | ||
} |
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
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
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,102 @@ | ||
package client | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/google/uuid" | ||
"github.com/prefecthq/terraform-provider-prefect/internal/api" | ||
) | ||
|
||
// type assertion ensures that this client implements the interface. | ||
var _ = api.AccountRolesClient(&AccountRolesClient{}) | ||
|
||
type AccountRolesClient struct { | ||
hc *http.Client | ||
apiKey string | ||
routePrefix string | ||
} | ||
|
||
// AccountRoles is a factory that initializes and returns a AccountRolesClient. | ||
// | ||
//nolint:ireturn // required to support PrefectClient mocking | ||
func (c *Client) AccountRoles(accountID uuid.UUID) (api.AccountRolesClient, error) { | ||
if accountID == uuid.Nil { | ||
accountID = c.defaultAccountID | ||
} | ||
|
||
return &AccountRolesClient{ | ||
hc: c.hc, | ||
apiKey: c.apiKey, | ||
routePrefix: fmt.Sprintf("%s/accounts/%s/account_roles", c.endpoint, accountID.String()), | ||
}, nil | ||
} | ||
|
||
// List returns a list of account roles, based on the provided filter. | ||
func (c *AccountRolesClient) List(ctx context.Context, roleNames []string) ([]*api.AccountRole, error) { | ||
var buf bytes.Buffer | ||
filterQuery := api.AccountRoleFilter{} | ||
filterQuery.AccountRoles.Name.Any = roleNames | ||
|
||
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 accountRoles []*api.AccountRole | ||
if err := json.NewDecoder(resp.Body).Decode(&accountRoles); err != nil { | ||
return nil, fmt.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
return accountRoles, nil | ||
} | ||
|
||
// Get returns an account role by ID. | ||
func (c *AccountRolesClient) Get(ctx context.Context, roleID uuid.UUID) (*api.AccountRole, error) { | ||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/%s", c.routePrefix, roleID.String()), 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 { | ||
errorBody, _ := io.ReadAll(resp.Body) | ||
|
||
return nil, fmt.Errorf("status code %s, error=%s", resp.Status, errorBody) | ||
} | ||
|
||
var accountRole api.AccountRole | ||
if err := json.NewDecoder(resp.Body).Decode(&accountRole); err != nil { | ||
return nil, fmt.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
return &accountRole, 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,172 @@ | ||
package datasources | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" | ||
"github.com/hashicorp/terraform-plugin-framework/datasource" | ||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema" | ||
"github.com/hashicorp/terraform-plugin-framework/schema/validator" | ||
"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 = &AccountRoleDataSource{} | ||
var _ datasource.DataSourceWithConfigure = &AccountRoleDataSource{} | ||
|
||
type AccountRoleDataSource struct { | ||
client api.PrefectClient | ||
} | ||
|
||
// AccountRoleDataSource defines the Terraform data source model | ||
// the TF data source configuration will be unmarshalled into this struct. | ||
type AccountRoleDataSourceModel struct { | ||
ID customtypes.UUIDValue `tfsdk:"id"` | ||
Created customtypes.TimestampValue `tfsdk:"created"` | ||
Updated customtypes.TimestampValue `tfsdk:"updated"` | ||
|
||
Name types.String `tfsdk:"name"` | ||
Permissions types.List `tfsdk:"permissions"` | ||
AccountID customtypes.UUIDValue `tfsdk:"account_id"` | ||
IsSystemRole types.Bool `tfsdk:"is_system_role"` | ||
} | ||
|
||
// NewWorkspaceRoleDataSource returns a new WorkspaceRoleDataSource. | ||
// | ||
//nolint:ireturn // required by Terraform API | ||
func NewAccountRoleDataSource() datasource.DataSource { | ||
return &AccountRoleDataSource{} | ||
} | ||
|
||
// Metadata returns the data source type name. | ||
func (d *AccountRoleDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { | ||
resp.TypeName = req.ProviderTypeName + "_account_role" | ||
} | ||
|
||
// Schema defines the schema for the data source. | ||
func (d *AccountRoleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { | ||
resp.Schema = schema.Schema{ | ||
Description: "Data Source representing a Prefect Workspace Role", | ||
Attributes: map[string]schema.Attribute{ | ||
"id": schema.StringAttribute{ | ||
Computed: true, | ||
CustomType: customtypes.UUIDType{}, | ||
Description: "Account Role UUID", | ||
}, | ||
"created": schema.StringAttribute{ | ||
Computed: true, | ||
CustomType: customtypes.TimestampType{}, | ||
Description: "Date and time of the Account Role creation in RFC 3339 format", | ||
}, | ||
"updated": schema.StringAttribute{ | ||
Computed: true, | ||
CustomType: customtypes.TimestampType{}, | ||
Description: "Date and time that the Account Role was last updated in RFC 3339 format", | ||
}, | ||
"name": schema.StringAttribute{ | ||
Required: true, | ||
Description: "Name of the Account Role", | ||
Validators: []validator.String{ | ||
stringvalidator.OneOf("Admin", "Member"), | ||
}, | ||
}, | ||
"permissions": schema.ListAttribute{ | ||
Computed: true, | ||
Description: "List of permissions linked to the Account Role", | ||
ElementType: types.StringType, | ||
}, | ||
"account_id": schema.StringAttribute{ | ||
Optional: true, | ||
CustomType: customtypes.UUIDType{}, | ||
Description: "Account UUID where Account Role resides", | ||
}, | ||
"is_system_role": schema.BoolAttribute{ | ||
Computed: true, | ||
Description: "Account UUID where Account Role resides", | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
// Configure adds the provider-configured client to the data source. | ||
func (d *AccountRoleDataSource) 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 *AccountRoleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { | ||
var config AccountRoleDataSourceModel | ||
|
||
// 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.AccountRoles(config.AccountID.ValueUUID()) | ||
if err != nil { | ||
resp.Diagnostics.Append(helpers.CreateClientErrorDiagnostic("Account Roles", err)) | ||
|
||
return | ||
} | ||
|
||
// Fetch an existing Account Role by name | ||
// Here, we'd expect only 1 Role (or none) to be returned | ||
// as we are querying a single Role name, not a list of names | ||
accountRoles, err := client.List(ctx, []string{config.Name.ValueString()}) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
"Error refreshing Workspace Role state", | ||
fmt.Sprintf("Could not read Workspace Role, unexpected error: %s", err.Error()), | ||
) | ||
} | ||
|
||
if len(accountRoles) != 1 { | ||
resp.Diagnostics.AddError( | ||
"Could not find Workspace Role", | ||
fmt.Sprintf("Could not find Workspace Role with name %s", config.Name.String()), | ||
) | ||
|
||
return | ||
} | ||
|
||
fetchedRole := accountRoles[0] | ||
|
||
config.ID = customtypes.NewUUIDValue(fetchedRole.ID) | ||
config.Created = customtypes.NewTimestampPointerValue(fetchedRole.Created) | ||
config.Updated = customtypes.NewTimestampPointerValue(fetchedRole.Updated) | ||
|
||
config.Name = types.StringValue(fetchedRole.Name) | ||
config.AccountID = customtypes.NewUUIDPointerValue(fetchedRole.AccountID) | ||
config.IsSystemRole = types.BoolValue(fetchedRole.IsSystemRole) | ||
|
||
list, diags := types.ListValueFrom(ctx, types.StringType, fetchedRole.Permissions) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
config.Permissions = list | ||
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
} |
Oops, something went wrong.