Skip to content
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

feat(blocks): add BlockDocument HTTP client #178

Merged
merged 5 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ linters:
- errchkjson
- errname
- errorlint
- execinquery
mitchnielsen marked this conversation as resolved.
Show resolved Hide resolved
- exhaustive
- exportloopref
- forbidigo
Expand All @@ -32,7 +31,7 @@ linters:
- godox
- goheader
- goimports
- gomnd
mitchnielsen marked this conversation as resolved.
Show resolved Hide resolved
- mnd
- gomoddirectives
- gomodguard
- goprintffuncname
Expand Down Expand Up @@ -76,7 +75,6 @@ linters:
- unconvert
- unused
- usestdlibvars
- varnamelen
mitchnielsen marked this conversation as resolved.
Show resolved Hide resolved
- wastedassign
- whitespace
- wrapcheck
Expand Down
67 changes: 67 additions & 0 deletions internal/api/block_documents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package api

import (
"context"

"github.com/google/uuid"
)

type BlockDocumentClient interface {
Get(ctx context.Context, id uuid.UUID) (*BlockDocument, error)
Create(ctx context.Context, payload BlockDocumentCreate) (*BlockDocument, error)
Update(ctx context.Context, id uuid.UUID, payload BlockDocumentUpdate) error
Delete(ctx context.Context, id uuid.UUID) error

GetACL(ctx context.Context, id uuid.UUID) (*BlockDocumentAccess, error)
UpdateACL(ctx context.Context, id uuid.UUID, payload BlockDocumentAccessReplace) error
}

type BlockDocument struct {
BaseModel
Name *string `json:"name"` // names are optional for anonymous blocks
Data string `json:"data"`

BlockSchemaID uuid.UUID `json:"block_schema_id"`
// BlockSchema *BlockSchema `json:"block_schema"`
mitchnielsen marked this conversation as resolved.
Show resolved Hide resolved

BlockTypeID uuid.UUID `json:"block_type_id"`
BlockTypeName *string `json:"block_type_name"`
BlockType BlockType `json:"block_type"`

BlockDocumentReferences string `json:"block_document_references"`

IsAnonymous bool `json:"is_anonymous"`
}

type BlockDocumentCreate struct {
Name *string `json:"name"` // names are optional for anonymous blocks
Data string `json:"data"`
BlockSchemaID uuid.UUID `json:"block_schema_id"`
BlockTypeID uuid.UUID `json:"block_type_id"`
IsAnonymous bool `json:"is_anonymous"`
}

type BlockDocumentUpdate struct {
BlockSchemaID *uuid.UUID `json:"block_schema_id"`
Data string `json:"data"`
MergeExistingData bool `json:"merge_existing_data"`
}

// BlockDocumentAccessReplace is the "update" request payload
// to modify a block document's current access control levels,
// meaning it contains the list of actors/teams + their respective access
// to a given block document.
type BlockDocumentAccessReplace struct {
ManageActorIDs []AccessActorID `json:"manage_actor_ids"`
ViewActorIDs []AccessActorID `json:"view_actor_ids"`
ManageTeamIDs []uuid.UUID `json:"manage_team_ids"`
ViewTeamIDs []uuid.UUID `json:"view_team_ids"`
}

// BlockDocumentAccess is the API object representing a
// block document's current access control levels
// by actor (user/team/service account) and role (manage/view).
type BlockDocumentAccess struct {
ManageActors []ObjectActorAccess `json:"manage_actors"`
ViewActors []ObjectActorAccess `json:"view_actors"`
}
1 change: 1 addition & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type PrefectClient interface {
Accounts(accountID uuid.UUID) (AccountsClient, error)
AccountMemberships(accountID uuid.UUID) (AccountMembershipsClient, error)
AccountRoles(accountID uuid.UUID) (AccountRolesClient, error)
BlockDocuments(accountID uuid.UUID, workspaceID uuid.UUID) (BlockDocumentClient, error)
BlockTypes(accountID uuid.UUID, workspaceID uuid.UUID) (BlockTypeClient, error)
Collections() (CollectionsClient, error)
Teams(accountID uuid.UUID) (TeamsClient, error)
Expand Down
78 changes: 78 additions & 0 deletions internal/api/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package api

import (
"encoding/json"
"fmt"

"github.com/google/uuid"
)

// AccessActorID is a custom type that represents
// an API response where the value can be:
// uuid.UUID - a single ID
// "*" - a wildcard string, meaning "all".
//
// nolint:musttag // we have custom marshal/unmarshal logic for this type
type AccessActorID struct {
ID *uuid.UUID
All bool
}
mitchnielsen marked this conversation as resolved.
Show resolved Hide resolved

// Custom JSON marshaling for AccessActorID
// so we can return uuid.UUID or "*" back to the API.
func (aid AccessActorID) MarshalJSON() ([]byte, error) {
if aid.All {
return []byte("*"), nil
}
if aid.ID != nil {
uuidByteSlice, err := json.Marshal(aid.ID)
if err != nil {
return nil, fmt.Errorf("failed to marshal AccessActorID: %w", err)
}

return uuidByteSlice, nil
}

return nil, fmt.Errorf("invalid AccessActorID: both ID and All are nil/false")
}

// Custom JSON unmarshaling for AccessActorID
// so we can accept uuid.UUID or "*" from the API
// in a structured format.
func (aid *AccessActorID) UnmarshalJSON(data []byte) error {
var id uuid.UUID
if err := json.Unmarshal(data, &id); err == nil {
aid.ID = &id
aid.All = false

return nil
}

var all string
if err := json.Unmarshal(data, &all); err == nil && all == "*" {
aid.All = true
aid.ID = nil

return nil
}

return fmt.Errorf("invalid AccessActorID format")
}

// AccessActorType represents an enum of type values
// used in our Access APIs.
type AccessActorType string

const (
UserAccessor AccessActorType = "user"
ServiceAccountAccessor AccessActorType = "service_account"
TeamAccessor AccessActorType = "team"
AllAccessors AccessActorType = "*"
)

type ObjectActorAccess struct {
ID AccessActorID `json:"id"`
Name string `json:"name"`
Email *string `json:"email"`
Type AccessActorType `json:"type"`
}
212 changes: 212 additions & 0 deletions internal/client/block_documents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
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.BlockDocumentClient(&BlockDocumentClient{})

type BlockDocumentClient struct {
hc *http.Client
apiKey string
routePrefix string
}

// BlockDocuments is a factory that initializes and returns a BlockDocumentClient.
//
//nolint:ireturn // required to support PrefectClient mocking
func (c *Client) BlockDocuments(accountID uuid.UUID, workspaceID uuid.UUID) (api.BlockDocumentClient, error) {
if accountID == uuid.Nil {
accountID = c.defaultAccountID
}

if workspaceID == uuid.Nil {
workspaceID = c.defaultWorkspaceID
}
if accountID == uuid.Nil && workspaceID == uuid.Nil {
return nil, fmt.Errorf("account id or workspace id is required")
}

return &BlockDocumentClient{
hc: c.hc,
apiKey: c.apiKey,
routePrefix: fmt.Sprintf("/account/%s/workspace/%s/block_documents", accountID.String(), workspaceID.String()),
}, nil
}

func (c *BlockDocumentClient) Get(ctx context.Context, id uuid.UUID) (*api.BlockDocument, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/%s", c.routePrefix, id.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 blockDocument api.BlockDocument
if err := json.NewDecoder(resp.Body).Decode(&blockDocument); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &blockDocument, nil
}

func (c *BlockDocumentClient) Create(ctx context.Context, payload api.BlockDocumentCreate) (*api.BlockDocument, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&payload); err != nil {
return nil, fmt.Errorf("failed to encode create payload data: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/", 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.StatusCreated {
errorBody, _ := io.ReadAll(resp.Body)

return nil, fmt.Errorf("status code %s, error=%s", resp.Status, errorBody)
}

var blockDocument api.BlockDocument
if err := json.NewDecoder(resp.Body).Decode(&blockDocument); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &blockDocument, nil
}

func (c *BlockDocumentClient) Update(ctx context.Context, id uuid.UUID, payload api.BlockDocumentUpdate) error {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&payload); err != nil {
return fmt.Errorf("failed to encode update payload data: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPatch, fmt.Sprintf("%s/%s", c.routePrefix, id.String()), &buf)
if err != nil {
return fmt.Errorf("error creating request: %w", err)
}

setDefaultHeaders(req, c.apiKey)

resp, err := c.hc.Do(req)
if err != nil {
return fmt.Errorf("http error: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent {
errorBody, _ := io.ReadAll(resp.Body)

return fmt.Errorf("status code %s, error=%s", resp.Status, errorBody)
}

return nil
}

func (c *BlockDocumentClient) Delete(ctx context.Context, id uuid.UUID) error {
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, fmt.Sprintf("%s/%s", c.routePrefix, id.String()), http.NoBody)
if err != nil {
return fmt.Errorf("error creating request: %w", err)
}

setDefaultHeaders(req, c.apiKey)

resp, err := c.hc.Do(req)
if err != nil {
return fmt.Errorf("http error: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent {
errorBody, _ := io.ReadAll(resp.Body)

return fmt.Errorf("status code %s, error=%s", resp.Status, errorBody)
}

return nil
}

func (c *BlockDocumentClient) GetACL(ctx context.Context, id uuid.UUID) (*api.BlockDocumentAccess, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/%s/access", c.routePrefix, id.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 blockDocumentAccess api.BlockDocumentAccess
if err := json.NewDecoder(resp.Body).Decode(&blockDocumentAccess); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &blockDocumentAccess, nil
}

func (c *BlockDocumentClient) UpdateACL(ctx context.Context, id uuid.UUID, payload api.
BlockDocumentAccessReplace) error {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&payload); err != nil {
return fmt.Errorf("failed to encode update payload data: %w", err)
}

req, err := http.NewRequestWithContext(ctx, http.MethodPut, fmt.Sprintf("%s/%s/access", c.routePrefix, id.String()), &buf)
if err != nil {
return fmt.Errorf("error creating request: %w", err)
}

setDefaultHeaders(req, c.apiKey)

resp, err := c.hc.Do(req)
if err != nil {
return fmt.Errorf("http error: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent {
errorBody, _ := io.ReadAll(resp.Body)

return fmt.Errorf("status code %s, error=%s", resp.Status, errorBody)
}

return nil
}
Loading