Skip to content

Commit

Permalink
initial teams support
Browse files Browse the repository at this point in the history
  • Loading branch information
triedandtested-dev committed Mar 11, 2024
1 parent bdf3026 commit dc8b1cf
Show file tree
Hide file tree
Showing 6 changed files with 452 additions and 0 deletions.
1 change: 1 addition & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type PrefectClient interface {
AccountMemberships(accountID uuid.UUID) (AccountMembershipsClient, error)
AccountRoles(accountID uuid.UUID) (AccountRolesClient, error)
Collections() (CollectionsClient, error)
Teams(accountID uuid.UUID) (TeamsClient, error)
Workspaces(accountID uuid.UUID) (WorkspacesClient, error)
WorkspaceAccess(accountID uuid.UUID, workspaceID uuid.UUID) (WorkspaceAccessClient, error)
WorkspaceRoles(accountID uuid.UUID) (WorkspaceRolesClient, error)
Expand Down
29 changes: 29 additions & 0 deletions internal/api/teams.go
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"]}}}.
type TeamFilter struct {
Teams struct {
Name struct {
Any []string `json:"any_"`
} `json:"name"`
} `json:"teams"`
}
73 changes: 73 additions & 0 deletions internal/client/teams.go
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) TeamsClient(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
}
155 changes: 155 additions & 0 deletions internal/provider/datasources/team.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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"`
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)",
},
"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.Name = types.StringValue(fetchedTeam.Name)
config.Description = types.StringValue(fetchedTeam.Description)

resp.Diagnostics.Append(resp.State.Set(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
}
37 changes: 37 additions & 0 deletions internal/provider/datasources/team_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package datasources_test

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/prefecthq/terraform-provider-prefect/internal/testutils"
)

func fixtureAccTeam(name string) string {
return fmt.Sprintf(`
data "prefect_team" "default" {
name = "%s"
}
`, name)
}

//nolint:paralleltest // we use the resource.ParallelTest helper instead
func TestAccDatasource_team(t *testing.T) {
dataSourceName := "data.prefect_team.default"

resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories,
PreCheck: func() { testutils.AccTestPreCheck(t) },
Steps: []resource.TestStep{
{
Config: fixtureAccTeam("TEST_NAME"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, "name", "TEST_NAME"),
resource.TestCheckResourceAttrSet(dataSourceName, "id"),
resource.TestCheckResourceAttrSet(dataSourceName, "description"),
),
},
},
})
}
Loading

0 comments on commit dc8b1cf

Please sign in to comment.