Skip to content

Commit

Permalink
fix: run logical db creation in provisioning flow instead of status c…
Browse files Browse the repository at this point in the history
…hecks
  • Loading branch information
jvmakine committed Nov 13, 2024
1 parent 8d0c162 commit 4e29e7f
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 53 deletions.
6 changes: 3 additions & 3 deletions cmd/ftl-provisioner-cloudformation/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ func (p *PostgresTemplater) AddToTemplate(template *goformation.Template) error
}
addOutput(template.Outputs, goformation.GetAtt(clusterID, "Endpoint.Address"), &CloudformationOutputKey{
ResourceID: p.resourceID,
PropertyName: PropertyDBWriteEndpoint,
PropertyName: PropertyPsqlWriteEndpoint,
})
addOutput(template.Outputs, goformation.GetAtt(clusterID, "ReadEndpoint.Address"), &CloudformationOutputKey{
ResourceID: p.resourceID,
PropertyName: PropertyDBReadEndpoint,
PropertyName: PropertyPsqlReadEndpoint,
})
addOutput(template.Outputs, goformation.GetAtt(clusterID, "MasterUserSecret.SecretArn"), &CloudformationOutputKey{
ResourceID: p.resourceID,
PropertyName: PropertyMasterUserARN,
PropertyName: PropertyPsqlMasterUserARN,
})
return nil
}
13 changes: 9 additions & 4 deletions cmd/ftl-provisioner-cloudformation/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import (
)

const (
PropertyDBReadEndpoint = "db:read_endpoint"
PropertyDBWriteEndpoint = "db:write_endpoint"
PropertyMasterUserARN = "db:master_user_secret_arn"
PropertyPsqlReadEndpoint = "psql:read_endpoint"
PropertyPsqlWriteEndpoint = "psql:write_endpoint"
PropertyPsqlMasterUserARN = "psql:master_user_secret_arn"
)

type Config struct {
Expand Down Expand Up @@ -55,7 +55,12 @@ func NewCloudformationProvisioner(ctx context.Context, config Config) (context.C
return nil, nil, fmt.Errorf("failed to create secretsmanager client: %w", err)
}

return ctx, &CloudformationProvisioner{client: client, secrets: secrets, confg: &config}, nil
return ctx, &CloudformationProvisioner{
client: client,
secrets: secrets,
confg: &config,
running: xsync.NewMapOf[string, *task](),
}, nil
}

func (c *CloudformationProvisioner) Ping(context.Context, *connect.Request[ftlv1.PingRequest]) (*connect.Response[ftlv1.PingResponse], error) {
Expand Down
47 changes: 5 additions & 42 deletions cmd/ftl-provisioner-cloudformation/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@ package main

import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/url"
"strings"

"connectrpc.com/connect"
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
_ "github.com/jackc/pgx/v5/stdlib" // SQL driver

"github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1beta1/provisioner"
Expand Down Expand Up @@ -105,31 +101,15 @@ func (c *CloudformationProvisioner) updatePostgresOutputs(ctx context.Context, t
return fmt.Errorf("failed to group outputs by property name: %w", err)
}

// TODO: Move to provisioner workflow
secretARN := *byName[PropertyMasterUserARN].OutputValue
username, password, err := c.secretARNToUsernamePassword(ctx, secretARN)
// TODO: mind the secret rotation
secretARN := *byName[PropertyPsqlMasterUserARN].OutputValue
username, password, err := secretARNToUsernamePassword(ctx, c.secrets, secretARN)
if err != nil {
return fmt.Errorf("failed to get username and password from secret ARN: %w", err)
}

to.ReadDsn = endpointToDSN(byName[PropertyDBReadEndpoint].OutputValue, resourceID, 5432, username, password)
to.WriteDsn = endpointToDSN(byName[PropertyDBWriteEndpoint].OutputValue, resourceID, 5432, username, password)
adminEndpoint := endpointToDSN(byName[PropertyDBReadEndpoint].OutputValue, "postgres", 5432, username, password)

// Connect to postgres without a specific database to create the new one
db, err := sql.Open("pgx", adminEndpoint)
if err != nil {
return fmt.Errorf("failed to connect to postgres: %w", err)
}
defer db.Close()

// Create the database if it doesn't exist
if _, err := db.ExecContext(ctx, "CREATE DATABASE "+resourceID); err != nil {
// Ignore if database already exists
if !strings.Contains(err.Error(), "already exists") {
return fmt.Errorf("failed to create database: %w", err)
}
}
to.ReadDsn = endpointToDSN(byName[PropertyPsqlReadEndpoint].OutputValue, resourceID, 5432, username, password)
to.WriteDsn = endpointToDSN(byName[PropertyPsqlWriteEndpoint].OutputValue, resourceID, 5432, username, password)

return nil
}
Expand All @@ -148,20 +128,3 @@ func endpointToDSN(endpoint *string, database string, port int, username, passwo

return url.String()
}

func (c *CloudformationProvisioner) secretARNToUsernamePassword(ctx context.Context, secretARN string) (string, string, error) {
secret, err := c.secrets.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{
SecretId: &secretARN,
})
if err != nil {
return "", "", fmt.Errorf("failed to get secret value: %w", err)
}
secretString := *secret.SecretString

var secretData map[string]string
if err := json.Unmarshal([]byte(secretString), &secretData); err != nil {
return "", "", fmt.Errorf("failed to unmarshal secret data: %w", err)
}

return secretData["username"], secretData["password"], nil
}
68 changes: 64 additions & 4 deletions cmd/ftl-provisioner-cloudformation/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package main

import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"

"github.com/alecthomas/atomic"
Expand Down Expand Up @@ -73,28 +76,85 @@ func (t *task) updateStack(ctx context.Context, client *cloudformation.Client, c
case types.StackStatusUpdateFailed:
return nil, fmt.Errorf("stack update failed: %s", *stack.StackStatusReason)
default:
return nil, fmt.Errorf("unsupported Cloudformation status code: %s", string(desc.Stacks[0].StackStatus))
return nil, fmt.Errorf("unsupported Cloudformation status code: %s", string(stack.StackStatus))
}

time.Sleep(retry.Duration())
}
}

func (t *task) postUpdate(ctx context.Context, client *cloudformation.Client, secrets *secretsmanager.Client, outputs []types.Output) error {
func (t *task) postUpdate(ctx context.Context, secrets *secretsmanager.Client, outputs []types.Output) error {
byResourceID, err := outputsByResourceID(outputs)
if err != nil {
return fmt.Errorf("failed to group outputs by resource ID: %w", err)
}

for resourceID, outputs := range byResourceID {
byName, err := outputsByPropertyName(outputs)
if err != nil {
return fmt.Errorf("failed to group outputs by property name: %w", err)
}

if write, ok := byName[PropertyPsqlWriteEndpoint]; ok {
if secret, ok := byName[PropertyPsqlMasterUserARN]; ok {
secretARN := *secret.OutputValue
username, password, err := secretARNToUsernamePassword(ctx, secrets, secretARN)
if err != nil {
return fmt.Errorf("failed to get username and password from secret ARN: %w", err)
}

adminEndpoint := endpointToDSN(write.OutputValue, "postgres", 5432, username, password)

// Connect to postgres without a specific database to create the new one
db, err := sql.Open("pgx", adminEndpoint)
if err != nil {
return fmt.Errorf("failed to connect to postgres: %w", err)
}
defer db.Close()

// Create the database if it doesn't exist
if _, err := db.ExecContext(ctx, "CREATE DATABASE "+resourceID); err != nil {
// Ignore if database already exists
if !strings.Contains(err.Error(), "already exists") {
return fmt.Errorf("failed to create database: %w", err)
}
}
}
}
}

return nil
}

func (t *task) Start(ctx context.Context, client *cloudformation.Client, secrets *secretsmanager.Client, changeSetID string) {
func (t *task) Start(oldCtx context.Context, client *cloudformation.Client, secrets *secretsmanager.Client, changeSetID string) {
ctx := context.WithoutCancel(oldCtx)
go func() {
outputs, err := t.updateStack(ctx, client, changeSetID)
if err != nil {
t.err.Store(err)
return
}
if err := t.postUpdate(ctx, client, secrets, outputs); err != nil {
if err := t.postUpdate(ctx, secrets, outputs); err != nil {
t.err.Store(err)
return
}
t.outputs.Store(outputs)
}()
}

func secretARNToUsernamePassword(ctx context.Context, secrets *secretsmanager.Client, secretARN string) (string, string, error) {
secret, err := secrets.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{
SecretId: &secretARN,
})
if err != nil {
return "", "", fmt.Errorf("failed to get secret value: %w", err)
}
secretString := *secret.SecretString

var secretData map[string]string
if err := json.Unmarshal([]byte(secretString), &secretData); err != nil {
return "", "", fmt.Errorf("failed to unmarshal secret data: %w", err)
}

return secretData["username"], secretData["password"], nil
}

0 comments on commit 4e29e7f

Please sign in to comment.