-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Teams initial support #146
Changes from all commits
dcb33f0
1d1753e
bdf3026
e2dc919
3512264
a176a89
dc82c5d
bf25671
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
# MacOS | ||
.DS_Store | ||
|
||
# Local Terraform provider cache and state | ||
.terraform | ||
terraform.tfstate | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
data "prefect_team" "my_team" { | ||
name = "my-team" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Query all Teams in Account | ||
data "prefect_teams" "all_teams" {} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -30,3 +30,13 @@ resource "prefect_workspace_access" "bot_developer" { | |||||
workspace_id = "00000000-0000-0000-0000-000000000000" | ||||||
workspace_role_id = data.prefect_workspace_role.developer.id | ||||||
} | ||||||
|
||||||
# ASSIGNING WORKSPACE ACCESS TO A TEAM | ||||||
|
||||||
# Assign the Workspace Role to the Team | ||||||
resource "prefect_workspace_access" "team_developer" { | ||||||
accessor_type = "TEAM" | ||||||
accessor_id = "11111111-1111-1111-1111-111111111111" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
just to reinforce that |
||||||
workspace_id = "00000000-0000-0000-0000-000000000000" | ||||||
workspace_role_id = data.prefect_workspace_role.developer.id | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package api | ||
|
||
import ( | ||
"context" | ||
) | ||
|
||
// TeamsClient is a client for working with teams. | ||
type TeamsClient interface { | ||
List(ctx context.Context, names []string) ([]*Team, error) | ||
} | ||
|
||
// Team is a representation of an team. | ||
type Team struct { | ||
BaseModel | ||
Name string `json:"name"` | ||
Description string `json:"description"` | ||
} | ||
|
||
// TeamFilter defines the search filter payload | ||
// when searching for team by name. | ||
// example request payload: | ||
// {"teams": {"name": {"any_": ["test"]}}}. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice |
||
type TeamFilter struct { | ||
Teams struct { | ||
Name struct { | ||
Any []string `json:"any_"` | ||
} `json:"name"` | ||
} `json:"teams"` | ||
} |
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.TeamsClient(&TeamsClient{}) | ||
|
||
type TeamsClient struct { | ||
hc *http.Client | ||
apiKey string | ||
routePrefix string | ||
} | ||
|
||
// Teams is a factory that initializes and returns a TeamsClient. | ||
// | ||
//nolint:ireturn // required to support PrefectClient mocking | ||
func (c *Client) Teams(accountID uuid.UUID) (api.TeamsClient, error) { | ||
if accountID == uuid.Nil { | ||
accountID = c.defaultAccountID | ||
} | ||
|
||
return &TeamsClient{ | ||
hc: c.hc, | ||
apiKey: c.apiKey, | ||
routePrefix: getAccountScopedURL(c.endpoint, accountID, "teams"), | ||
}, nil | ||
} | ||
|
||
// List returns a list of teams, based on the provided filter. | ||
func (c *TeamsClient) List(ctx context.Context, names []string) ([]*api.Team, error) { | ||
var buf bytes.Buffer | ||
filterQuery := api.TeamFilter{} | ||
filterQuery.Teams.Name.Any = names | ||
|
||
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 teams []*api.Team | ||
if err := json.NewDecoder(resp.Body).Decode(&teams); err != nil { | ||
return nil, fmt.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
return teams, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
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 = &TeamDataSource{} | ||
var _ datasource.DataSourceWithConfigure = &TeamDataSource{} | ||
|
||
type TeamDataSource struct { | ||
client api.PrefectClient | ||
} | ||
|
||
type TeamDataSourceModel struct { | ||
ID customtypes.UUIDValue `tfsdk:"id"` | ||
Created customtypes.TimestampValue `tfsdk:"created"` | ||
Updated customtypes.TimestampValue `tfsdk:"updated"` | ||
Name types.String `tfsdk:"first_name"` | ||
Description types.String `tfsdk:"last_name"` | ||
|
||
AccountID customtypes.UUIDValue `tfsdk:"account_id"` | ||
} | ||
|
||
// NewTeamDataSource returns a new TeamDataSource. | ||
// | ||
//nolint:ireturn // required by Terraform API | ||
func NewTeamDataSource() datasource.DataSource { | ||
return &TeamDataSource{} | ||
} | ||
|
||
// Metadata returns the data source type name. | ||
func (d *TeamDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { | ||
resp.TypeName = req.ProviderTypeName + "_team" | ||
} | ||
|
||
// Shared set of schema attributes between team (singular) | ||
// and teams (plural) datasources. Any team (singular) | ||
// specific attributes will be added to a deep copy in the Schema method. | ||
var teamAttributesBase = map[string]schema.Attribute{ | ||
"id": schema.StringAttribute{ | ||
Computed: true, | ||
CustomType: customtypes.UUIDType{}, | ||
Description: "Team ID (UUID)", | ||
}, | ||
"created": schema.StringAttribute{ | ||
Computed: true, | ||
CustomType: customtypes.TimestampType{}, | ||
Description: "Date and time of the team creation in RFC 3339 format", | ||
}, | ||
"updated": schema.StringAttribute{ | ||
Computed: true, | ||
CustomType: customtypes.TimestampType{}, | ||
Description: "Date and time that the team was last updated in RFC 3339 format", | ||
}, | ||
"name": schema.StringAttribute{ | ||
Computed: true, | ||
Description: "Name of Team", | ||
}, | ||
"description": schema.StringAttribute{ | ||
Computed: true, | ||
Description: "Description of team", | ||
}, | ||
} | ||
|
||
// Schema defines the schema for the data source. | ||
func (d *TeamDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { | ||
|
||
// Create a copy of the base attributes | ||
// and add the account ID overrides here | ||
// as they are not needed in the teams (plural) list | ||
teamAttributes := make(map[string]schema.Attribute) | ||
for k, v := range workPoolAttributesBase { | ||
teamAttributes[k] = v | ||
} | ||
teamAttributes["account_id"] = schema.StringAttribute{ | ||
CustomType: customtypes.UUIDType{}, | ||
Description: "Account ID (UUID), defaults to the account set in the provider", | ||
Optional: true, | ||
} | ||
|
||
resp.Schema = schema.Schema{ | ||
Description: ` | ||
Get information about an existing Team by their name. | ||
<br> | ||
Use this data source to obtain team IDs to manage Workspace Access. | ||
`, | ||
Attributes: teamAttributes, | ||
} | ||
} | ||
|
||
// Configure adds the provider-configured client to the data source. | ||
func (d *TeamDataSource) 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 *TeamDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { | ||
var config TeamDataSourceModel | ||
|
||
// 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.Teams(config.AccountID.ValueUUID()) | ||
if err != nil { | ||
resp.Diagnostics.Append(helpers.CreateClientErrorDiagnostic("Teams", err)) | ||
|
||
return | ||
} | ||
|
||
// Fetch an existing Team by name | ||
// Here, we'd expect only 1 Team (or none) to be returned | ||
// as we are querying a single Team name, not a list of names | ||
// teams, err := client.List(ctx, []string{model.Name.ValueString()}) | ||
teams, err := client.List(ctx, []string{config.Name.ValueString()}) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
"Error refreshing Team state", | ||
fmt.Sprintf("Could not search for Team, unexpected error: %s", err.Error()), | ||
) | ||
} | ||
|
||
if len(teams) != 1 { | ||
resp.Diagnostics.AddError( | ||
"Could not find Team", | ||
fmt.Sprintf("Could not find Team with name %s", config.Name.ValueString()), | ||
) | ||
|
||
return | ||
} | ||
|
||
fetchedTeam := teams[0] | ||
|
||
config.ID = customtypes.NewUUIDValue(fetchedTeam.ID) | ||
config.Created = customtypes.NewTimestampPointerValue(fetchedTeam.Created) | ||
config.Updated = customtypes.NewTimestampPointerValue(fetchedTeam.Updated) | ||
config.Name = types.StringValue(fetchedTeam.Name) | ||
config.Description = types.StringValue(fetchedTeam.Description) | ||
|
||
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍