From 4e29e7f8ff71eed5e4e3a5df1aa1a1c7f310a2da Mon Sep 17 00:00:00 2001 From: Juho Makinen Date: Wed, 13 Nov 2024 15:39:14 +1100 Subject: [PATCH] fix: run logical db creation in provisioning flow instead of status checks --- .../postgres.go | 6 +- .../provisioner.go | 13 ++-- cmd/ftl-provisioner-cloudformation/status.go | 47 ++----------- cmd/ftl-provisioner-cloudformation/task.go | 68 +++++++++++++++++-- 4 files changed, 81 insertions(+), 53 deletions(-) diff --git a/cmd/ftl-provisioner-cloudformation/postgres.go b/cmd/ftl-provisioner-cloudformation/postgres.go index 408742260..d867e2a0f 100644 --- a/cmd/ftl-provisioner-cloudformation/postgres.go +++ b/cmd/ftl-provisioner-cloudformation/postgres.go @@ -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 } diff --git a/cmd/ftl-provisioner-cloudformation/provisioner.go b/cmd/ftl-provisioner-cloudformation/provisioner.go index 5f7c72aa1..ee0581d19 100644 --- a/cmd/ftl-provisioner-cloudformation/provisioner.go +++ b/cmd/ftl-provisioner-cloudformation/provisioner.go @@ -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 { @@ -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) { diff --git a/cmd/ftl-provisioner-cloudformation/status.go b/cmd/ftl-provisioner-cloudformation/status.go index ee69e4417..7409eecd3 100644 --- a/cmd/ftl-provisioner-cloudformation/status.go +++ b/cmd/ftl-provisioner-cloudformation/status.go @@ -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" @@ -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 } @@ -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 -} diff --git a/cmd/ftl-provisioner-cloudformation/task.go b/cmd/ftl-provisioner-cloudformation/task.go index 481f7bef3..90df8c093 100644 --- a/cmd/ftl-provisioner-cloudformation/task.go +++ b/cmd/ftl-provisioner-cloudformation/task.go @@ -2,7 +2,10 @@ package main import ( "context" + "database/sql" + "encoding/json" "fmt" + "strings" "time" "github.com/alecthomas/atomic" @@ -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 +}