feat: resources segregation by tenant
All checks were successful
arcad/emissary/pipeline/head This commit looks good
arcad/emissary/pipeline/pr-master This commit looks good

This commit is contained in:
2024-02-26 18:20:40 +01:00
parent 79f53010a0
commit ca4211daef
45 changed files with 704 additions and 429 deletions

View File

@ -29,6 +29,7 @@ type Agent struct {
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ContactedAt *time.Time `json:"contactedAt,omitempty"`
TenantID *TenantID `json:"tenantId"`
}
type SerializableKeySet struct {

View File

@ -9,6 +9,10 @@ import (
type AgentRepository interface {
Create(ctx context.Context, thumbprint string, keySet jwk.Set, metadata map[string]any) (*Agent, error)
Attach(ctx context.Context, tenantID TenantID, agentID AgentID) (*Agent, error)
Detach(ctx context.Context, agentID AgentID) (*Agent, error)
Get(ctx context.Context, id AgentID) (*Agent, error)
Update(ctx context.Context, id AgentID, updates ...AgentUpdateOptionFunc) (*Agent, error)
Query(ctx context.Context, opts ...AgentQueryOptionFunc) ([]*Agent, int, error)
@ -25,6 +29,7 @@ type AgentQueryOptions struct {
Limit *int
Offset *int
IDs []AgentID
TenantIDs []TenantID
Thumbprints []string
Metadata *map[string]any
Statuses []AgentStatus
@ -54,6 +59,12 @@ func WithAgentQueryID(ids ...AgentID) AgentQueryOptionFunc {
}
}
func WithAgentQueryTenantID(ids ...TenantID) AgentQueryOptionFunc {
return func(opts *AgentQueryOptions) {
opts.TenantIDs = ids
}
}
func WithAgentQueryStatus(statuses ...AgentStatus) AgentQueryOptionFunc {
return func(opts *AgentQueryOptions) {
opts.Statuses = statuses
@ -75,6 +86,13 @@ type AgentUpdateOptions struct {
Metadata *map[string]any
KeySet *jwk.Set
Thumbprint *string
TenantID *TenantID
}
func WithAgentUpdateTenant(id TenantID) AgentUpdateOptionFunc {
return func(opts *AgentUpdateOptions) {
opts.TenantID = &id
}
}
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {

View File

@ -6,4 +6,5 @@ var (
ErrNotFound = errors.New("not found")
ErrAlreadyExist = errors.New("already exist")
ErrUnexpectedRevision = errors.New("unexpected revision")
ErrAlreadyAttached = errors.New("already attached")
)

View File

@ -15,6 +15,8 @@ type Spec struct {
Revision int `json:"revision"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
TenantID TenantID `json:"tenantId"`
AgentID AgentID `json:"agentId"`
}
func (s *Spec) SpecName() spec.Name {

View File

@ -20,6 +20,75 @@ type AgentRepository struct {
sqliteBusyRetryMaxAttempts int
}
// Attach implements datastore.AgentRepository.
func (r *AgentRepository) Attach(ctx context.Context, tenantID datastore.TenantID, agentID datastore.AgentID) (*datastore.Agent, error) {
var agent datastore.Agent
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
query := `SELECT count(id), tenant_id FROM agents WHERE id = $1`
row := tx.QueryRowContext(ctx, query, agentID)
var (
count int
attachedTenantID *datastore.TenantID
)
if err := row.Scan(&count, &attachedTenantID); err != nil {
return errors.WithStack(err)
}
if count == 0 {
return errors.WithStack(datastore.ErrNotFound)
}
if attachedTenantID != nil {
return errors.WithStack(datastore.ErrAlreadyAttached)
}
now := time.Now().UTC()
query = `
UPDATE agents SET tenant_id = $1, updated_at = $2
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
`
row = tx.QueryRowContext(
ctx, query,
tenantID,
now,
)
metadata := JSONMap{}
var rawKeySet []byte
err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID)
if err != nil {
return errors.WithStack(err)
}
agent.Metadata = metadata
keySet, err := jwk.Parse(rawKeySet)
if err != nil {
return errors.WithStack(err)
}
agent.KeySet = &datastore.SerializableKeySet{keySet}
return nil
})
if err != nil {
return nil, errors.WithStack(err)
}
return &agent, nil
}
// Detach implements datastore.AgentRepository.
func (*AgentRepository) Detach(ctx context.Context, agentID datastore.AgentID) (*datastore.Agent, error) {
panic("unimplemented")
}
// DeleteSpec implements datastore.AgentRepository.
func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.AgentID, name string) error {
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
@ -170,7 +239,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
count := 0
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
query := `SELECT id, label, thumbprint, status, contacted_at, created_at, updated_at FROM agents`
query := `SELECT id, label, thumbprint, status, contacted_at, created_at, updated_at, tenant_id FROM agents`
limit := 10
if options.Limit != nil {
@ -193,6 +262,13 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
args = append(args, newArgs...)
}
if options.TenantIDs != nil && len(options.TenantIDs) > 0 {
filter, newArgs, newParamIndex := inFilter("tenant_id", paramIndex, options.TenantIDs)
filters += filter
paramIndex = newParamIndex
args = append(args, newArgs...)
}
if options.Thumbprints != nil && len(options.Thumbprints) > 0 {
if filters != "" {
filters += " AND "
@ -240,7 +316,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
metadata := JSONMap{}
contactedAt := sql.NullTime{}
if err := rows.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if err := rows.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID); err != nil {
return errors.WithStack(err)
}
@ -293,7 +369,7 @@ func (r *AgentRepository) Create(ctx context.Context, thumbprint string, keySet
query = `
INSERT INTO agents (thumbprint, keyset, metadata, status, created_at, updated_at)
VALUES($1, $2, $3, $4, $5, $5)
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
`
rawKeySet, err := json.Marshal(keySet)
@ -308,7 +384,7 @@ func (r *AgentRepository) Create(ctx context.Context, thumbprint string, keySet
metadata := JSONMap{}
err = row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt)
err = row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID)
if err != nil {
return errors.WithStack(err)
}
@ -363,7 +439,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
query := `
SELECT "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
SELECT "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at", "tenant_id"
FROM agents
WHERE id = $1
`
@ -374,7 +450,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
contactedAt := sql.NullTime{}
var rawKeySet []byte
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return datastore.ErrNotFound
}
@ -476,7 +552,7 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
query += `
WHERE id = $1
RETURNING "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
RETURNING "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at", "tenant_id"
`
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
@ -487,7 +563,7 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
contactedAt := sql.NullTime{}
var rawKeySet []byte
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return datastore.ErrNotFound
}
@ -622,23 +698,3 @@ func NewAgentRepository(db *sql.DB, sqliteBusyRetryMaxAttempts int) *AgentReposi
}
var _ datastore.AgentRepository = &AgentRepository{}
func inFilter[T any](column string, paramIndex int, items []T) (string, []any, int) {
args := make([]any, 0, len(items))
filter := fmt.Sprintf("%s in (", column)
for idx, item := range items {
if idx != 0 {
filter += ","
}
filter += fmt.Sprintf("$%d", paramIndex)
paramIndex++
args = append(args, item)
}
filter += ")"
return filter, args, paramIndex
}

View File

@ -0,0 +1,23 @@
package sqlite
import "fmt"
func inFilter[T any](column string, paramIndex int, items []T) (string, []any, int) {
args := make([]any, 0, len(items))
filter := fmt.Sprintf("%s in (", column)
for idx, item := range items {
if idx != 0 {
filter += ","
}
filter += fmt.Sprintf("$%d", paramIndex)
paramIndex++
args = append(args, item)
}
filter += ")"
return filter, args, paramIndex
}

View File

@ -0,0 +1,32 @@
package datastore
import (
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
)
const DefaultTenantID TenantID = "00000000-0000-0000-0000-000000000000"
type TenantID string
func NewTenantID() TenantID {
return TenantID(uuid.New().String())
}
func ParseTenantID(raw string) (TenantID, error) {
uuid, err := uuid.Parse(raw)
if err != nil {
return "", errors.WithStack(err)
}
return TenantID(uuid.String()), nil
}
type Tenant struct {
ID TenantID `json:"id"`
Label string `json:"label"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}