-
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: Service Accounts Datasource + Resource (#49)
* Add service account API calls * Add SA structs for request payloads * Fix ServiceAccount client * Fix return type for SA client * Add json fields to structs for unmarshalling * Add ServiceAccounts to PrefectClient interface * Fix API calls to use the SA client struct properly * Move getServiceAccountScopedURL to util file and restructure to align with other url functions * Use getAccountScopedURL to generate the SA route prefix * Fix route prefix for SA * Add skeleton'ish service account tf resource * Add more fields of SA resource structure * SA resource * New ServiceAccount struct * Remove redundant structs * Update ServiceAccountClient return types * List client function for service accounts * Spacing * Align struct names with other resources * Test * test * Import google uuid for SA * Add temporary replace directive to go.mod for development * Separate structs to handle service account List responses * Add Request suffix to all SA request structs. Remove response marshaling for SA Update * Refactor client.Create to use NewRequestWithContext * Refactor Get & Update client calls to use NewRequestWithContext * Refactor Delete client call * Fix struct naming for ServiceAccountNoKey * Begin implementing service_account resource code * Struct changes * todo note * Implement function to copy ServiceAccount data to a ServiceAccountResourceModel * Create tf operation for service accounts - base structure * Implement tfsdk Read operation for service account resource * Implement tfsdk Update operation for service accounts * Implement Delete and Import tfsdk operations for Service Accounts * Fix package in SA resource code * Add base structure for service account datasource implementation - incomplete * Adds SA attributres for SA datasource * Assign the sa attributes to the model in the datasource * Add placeholder install target * Remove unused imports. Fix ServiceAccountsClient errors * Remove unused imports from sa datasource. Fix references to ServiceAccountSourceModel * Increase test fail limit. Remove unused resources from sa resource * Fix references c->sa * Fix sa.Name string() call * Comment out diags reference for testing * Change AccountId to uuid custom type in the response block * Cast AccountRoleId to string literal * Ensure the sa client unmarshals directly to ServiceAccount object, no longer requiring ServiceAccountNoKey * Linter fixes + added example SA definition in examples/ * Linter fixes * Add SA resource and data source items to the provider * Set vars for accountid and api key, and a dummy tfvars file * Update ServiceAccount client contructor to set accountID to default (global) if not specified in the resource * Update SA client create function to embed the response body into the error message. * Handle optional fields in the create payload * Update comment for setting APIKeyExpiration * Add better http status code handling. Fix bug in client Get method * linting * add service accoutn datasource acc tests * minor fixes arising from tests * ignore lint --------- Co-authored-by: dev-adeebr <[email protected]> Co-authored-by: Edward Park <[email protected]>
- Loading branch information
1 parent
40eb98b
commit be52e21
Showing
19 changed files
with
937 additions
and
56 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 |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
terraform.tfstate | ||
terraform.tfstate.*.backup | ||
terraform.tfstate.backup | ||
local.tfvars | ||
|
||
# Built artifacts | ||
build/* |
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,5 +1,5 @@ | ||
resource "prefect_work_pool" "abc" { | ||
name = "testpool" | ||
type = "kubernetes" | ||
paused = false | ||
} | ||
#resource "prefect_work_pool" "abc" { | ||
# name = "testpool" | ||
# type = "kubernetes" | ||
# paused = false | ||
#} |
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,3 @@ | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,4 +8,6 @@ terraform { | |
} | ||
|
||
provider "prefect" { | ||
api_key = var.PREFECT_API_KEY | ||
account_id = var.PREFECT_ACCOUNT_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,3 @@ | ||
resource "prefect_service_account" "sa_example" { | ||
name = "beautiful-service-account-example" | ||
} |
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,14 @@ | ||
variable "PREFECT_API_KEY" { | ||
description = "The Prefect API key" | ||
type = string | ||
} | ||
|
||
variable "PREFECT_ACCOUNT_ID" { | ||
description = "The Prefect Account ID" | ||
type = string | ||
} | ||
|
||
variable "PREFECT_API_URL" { | ||
description = "The Prefect API url" | ||
type = string | ||
} |
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
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,69 @@ | ||
package api | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"github.com/google/uuid" | ||
) | ||
|
||
type ServiceAccountsClient interface { | ||
Create(ctx context.Context, request ServiceAccountCreateRequest) (*ServiceAccount, error) | ||
List(ctx context.Context, filter ServiceAccountFilterRequest) ([]*ServiceAccount, error) | ||
Get(ctx context.Context, name string) (*ServiceAccount, error) | ||
Update(ctx context.Context, name string, data ServiceAccountUpdateRequest) error | ||
Delete(ctx context.Context, name string) error | ||
} | ||
|
||
/*** REQUEST DATA STRUCTS ***/ | ||
|
||
type ServiceAccountCreateRequest struct { | ||
Name string `json:"name"` | ||
APIKeyExpiration string `json:"api_key_expiration,omitempty"` | ||
AccountRoleID string `json:"account_role_id,omitempty"` | ||
} | ||
|
||
type ServiceAccountUpdateRequest struct { | ||
Name string `json:"name"` | ||
} | ||
|
||
type ServiceAccountFilterRequest struct { | ||
Any []uuid.UUID `json:"any_"` | ||
} | ||
|
||
/*** RESPONSE DATA STRUCTS ***/ | ||
|
||
// ServiceAccount is a representation of a created service account (from a Create response). | ||
type ServiceAccount struct { | ||
BaseModel | ||
AccountID uuid.UUID `json:"account_id"` | ||
Name string `json:"name"` | ||
AccountRoleName string `json:"account_role_name"` | ||
APIKey ServiceAccountAPIKey `json:"api_key"` | ||
} | ||
|
||
type ServiceAccountAPIKey struct { | ||
ID string `json:"id"` | ||
Created *time.Time `json:"created"` | ||
Name string `json:"name"` | ||
Expiration *time.Time `json:"expiration"` | ||
Key string `json:"key"` | ||
} | ||
|
||
// ServiceAccountNoKey is a representation of Service Account details obtained from a List/Filter | ||
// and excludes the actual key value for the api_key. | ||
type ServiceAccountNoKey struct { | ||
BaseModel | ||
AccountID uuid.UUID `json:"account_id"` | ||
Name string `json:"name"` | ||
AccountRoleName string `json:"account_role_name"` | ||
APIKey ServiceAccountAPIKeyNoKey `json:"api_key"` | ||
} | ||
|
||
// Represents an api_key block received from a List/Filter response, which excludes the actual key. | ||
type ServiceAccountAPIKeyNoKey struct { | ||
ID string `json:"id"` | ||
Created *time.Time `json:"created"` | ||
Name string `json:"name"` | ||
Expiration *time.Time `json:"expiration"` | ||
} |
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,188 @@ | ||
package client | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/google/uuid" | ||
"github.com/prefecthq/terraform-provider-prefect/internal/api" | ||
) | ||
|
||
type ServiceAccountsClient struct { | ||
hc *http.Client | ||
apiKey string | ||
routePrefix string | ||
} | ||
|
||
//nolint:ireturn // required to support PrefectClient mocking | ||
func (c *Client) ServiceAccounts(accountID uuid.UUID) (api.ServiceAccountsClient, error) { | ||
if c.apiKey == "" { | ||
return nil, fmt.Errorf("apiKey is not set") | ||
} | ||
|
||
if c.endpoint == "" { | ||
return nil, fmt.Errorf("endpoint is not set") | ||
} | ||
|
||
if accountID == uuid.Nil { | ||
accountID = c.defaultAccountID | ||
} | ||
|
||
// Since service accounts are account scoped. Generate from util.getAccountScopedURL | ||
// e.g. this will generate routePrefix ending in /accounts/bots | ||
routePrefix := getAccountScopedURL(c.endpoint, accountID, "bots") | ||
|
||
return &ServiceAccountsClient{ | ||
hc: c.hc, | ||
apiKey: c.apiKey, | ||
routePrefix: routePrefix, | ||
}, nil | ||
} | ||
|
||
func (sa *ServiceAccountsClient) Create(ctx context.Context, request api.ServiceAccountCreateRequest) (*api.ServiceAccount, error) { | ||
var buf bytes.Buffer | ||
if err := json.NewEncoder(&buf).Encode(&request); err != nil { | ||
return nil, fmt.Errorf("failed to encode request data: %w", err) | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, sa.routePrefix+"/", &buf) | ||
if err != nil { | ||
return nil, fmt.Errorf("error creating request: %w", err) | ||
} | ||
|
||
setDefaultHeaders(req, sa.apiKey) | ||
|
||
resp, err := sa.hc.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("http error: %w", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusCreated { | ||
// Read the response body | ||
bodyBytes, _ := io.ReadAll(resp.Body) | ||
bodyString := string(bodyBytes) | ||
|
||
return nil, fmt.Errorf("status code: %s, response body: %s", resp.Status, bodyString) | ||
} | ||
|
||
var response api.ServiceAccount | ||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { | ||
return nil, fmt.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
return &response, nil | ||
} | ||
|
||
func (sa *ServiceAccountsClient) List(ctx context.Context, filter api.ServiceAccountFilterRequest) ([]*api.ServiceAccount, error) { | ||
var buf bytes.Buffer | ||
if err := json.NewEncoder(&buf).Encode(&filter); err != nil { | ||
return nil, fmt.Errorf("failed to encode filter: %w", err) | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, sa.routePrefix+"/filter", &buf) | ||
if err != nil { | ||
return nil, fmt.Errorf("error creating request: %w", err) | ||
} | ||
|
||
setDefaultHeaders(req, sa.apiKey) | ||
|
||
resp, err := sa.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 serviceAccounts []*api.ServiceAccount | ||
if err := json.NewDecoder(resp.Body).Decode(&serviceAccounts); err != nil { // THIS IS THE RESPONSE | ||
return nil, fmt.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
return serviceAccounts, nil | ||
} | ||
|
||
func (sa *ServiceAccountsClient) Get(ctx context.Context, botID string) (*api.ServiceAccount, error) { | ||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sa.routePrefix+"/"+botID, http.NoBody) | ||
if err != nil { | ||
return nil, fmt.Errorf("error creating request: %w", err) | ||
} | ||
|
||
setDefaultHeaders(req, sa.apiKey) | ||
|
||
resp, err := sa.hc.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("http error: %w", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
switch resp.StatusCode { | ||
case http.StatusOK: | ||
case http.StatusNotFound: | ||
return nil, fmt.Errorf("could not find Service Account") | ||
default: | ||
bodyBytes, _ := io.ReadAll(resp.Body) | ||
|
||
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(bodyBytes)) | ||
} | ||
|
||
var response api.ServiceAccount | ||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { | ||
return nil, fmt.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
return &response, nil | ||
} | ||
|
||
func (sa *ServiceAccountsClient) Update(ctx context.Context, botID string, request api.ServiceAccountUpdateRequest) error { | ||
var buf bytes.Buffer | ||
if err := json.NewEncoder(&buf).Encode(&request); err != nil { | ||
return fmt.Errorf("failed to encode request data: %w", err) | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, sa.routePrefix+"/"+botID, &buf) | ||
if err != nil { | ||
return fmt.Errorf("error creating request: %w", err) | ||
} | ||
|
||
setDefaultHeaders(req, sa.apiKey) | ||
|
||
resp, err := sa.hc.Do(req) | ||
if err != nil { | ||
return fmt.Errorf("http error: %w", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { | ||
return fmt.Errorf("status code %s", resp.Status) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (sa *ServiceAccountsClient) Delete(ctx context.Context, botID string) error { | ||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, sa.routePrefix+"/"+botID, http.NoBody) | ||
if err != nil { | ||
return fmt.Errorf("error creating request: %w", err) | ||
} | ||
setDefaultHeaders(req, sa.apiKey) | ||
|
||
resp, err := sa.hc.Do(req) | ||
if err != nil { | ||
return fmt.Errorf("http error: %w", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { | ||
return fmt.Errorf("status code %s", resp.Status) | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.