Skip to content

Commit 86481d0

Browse files
author
asib
authored
Restore code for handling launch of unmanaged PG (#4371)
* restore code for handling launch of unmanaged PG * don't fetch LD client from ctx (there doesn't seem to be one there), build a new service client instead
1 parent f2cfb3c commit 86481d0

File tree

6 files changed

+146
-119
lines changed

6 files changed

+146
-119
lines changed

internal/command/launch/cmd.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/superfly/flyctl/internal/flag"
2424
"github.com/superfly/flyctl/internal/flyerr"
2525
"github.com/superfly/flyctl/internal/flyutil"
26+
"github.com/superfly/flyctl/internal/launchdarkly"
2627
"github.com/superfly/flyctl/internal/metrics"
2728
"github.com/superfly/flyctl/internal/prompt"
2829
"github.com/superfly/flyctl/internal/state"
@@ -40,7 +41,7 @@ func New() (cmd *cobra.Command) {
4041
cmd = command.New("launch", short, long, run, command.RequireSession, command.RequireUiex, command.LoadAppConfigIfPresent)
4142
cmd.Args = cobra.NoArgs
4243

43-
flag.Add(cmd,
44+
flags := []flag.Flag{
4445
// Since launch can perform a deployment, we offer the full set of deployment flags for those using
4546
// the launch command in CI environments. We may want to rescind this decision down the line, because
4647
// the list of flags is long, but it follows from the precedent of already offering some deployment flags.
@@ -118,11 +119,6 @@ func New() (cmd *cobra.Command) {
118119
Description: "Skip automatically provisioning a database",
119120
Default: false,
120121
},
121-
flag.Bool{
122-
Name: "db",
123-
Description: "Force provisioning a managed Postgres database",
124-
Default: false,
125-
},
126122
flag.Bool{
127123
Name: "no-redis",
128124
Description: "Skip automatically provisioning a Redis instance",
@@ -150,7 +146,23 @@ func New() (cmd *cobra.Command) {
150146
Description: "Automatically suspend the app after a period of inactivity. Valid values are 'off', 'stop', and 'suspend",
151147
Default: "stop",
152148
},
153-
)
149+
}
150+
151+
ldClient, err := launchdarkly.NewServiceClient()
152+
if err != nil {
153+
return nil
154+
}
155+
156+
managedPostgresEnabled := ldClient.ManagedPostgresEnabled()
157+
if managedPostgresEnabled {
158+
flags = append(flags, flag.Bool{
159+
Name: "db",
160+
Description: "Force provisioning a managed Postgres database",
161+
Default: false,
162+
})
163+
}
164+
165+
flag.Add(cmd, flags...)
154166

155167
cmd.AddCommand(NewPlan())
156168

internal/command/launch/describe_plan.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
const descriptionNone = "<none>"
1818

1919
func describePostgresPlan(launchPlan *plan.LaunchPlan) (string, error) {
20-
2120
switch provider := launchPlan.Postgres.Provider().(type) {
2221
case *plan.FlyPostgresPlan:
2322
return describeFlyPostgresPlan(provider)

internal/command/launch/launch_databases.go

Lines changed: 73 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import (
77

88
"github.com/samber/lo"
99
fly "github.com/superfly/fly-go"
10+
"github.com/superfly/flyctl/flypg"
1011
"github.com/superfly/flyctl/gql"
1112
extensions_core "github.com/superfly/flyctl/internal/command/extensions/core"
1213
"github.com/superfly/flyctl/internal/command/launch/plan"
1314
"github.com/superfly/flyctl/internal/command/mpg"
15+
"github.com/superfly/flyctl/internal/command/postgres"
1416
"github.com/superfly/flyctl/internal/command/redis"
1517
"github.com/superfly/flyctl/internal/flyutil"
1618
"github.com/superfly/flyctl/internal/uiex"
@@ -74,124 +76,90 @@ func (state *launchState) createDatabases(ctx context.Context) error {
7476

7577
func (state *launchState) createFlyPostgres(ctx context.Context) error {
7678
var (
77-
io = iostreams.FromContext(ctx)
78-
pgPlan = state.Plan.Postgres.FlyPostgres
79-
uiexClient = uiexutil.ClientFromContext(ctx)
79+
pgPlan = state.Plan.Postgres.FlyPostgres
80+
apiClient = flyutil.ClientFromContext(ctx)
81+
io = iostreams.FromContext(ctx)
8082
)
8183

82-
// Get org and region
83-
org, err := state.Org(ctx)
84-
if err != nil {
85-
return err
86-
}
87-
region, err := state.Region(ctx)
88-
if err != nil {
89-
return err
90-
}
91-
92-
var slug string
93-
if org.Slug == "personal" {
94-
genqClient := flyutil.ClientFromContext(ctx).GenqClient()
95-
96-
// For ui-ex request we need the real org slug
97-
var fullOrg *gql.GetOrganizationResponse
98-
if fullOrg, err = gql.GetOrganization(ctx, genqClient, org.Slug); err != nil {
99-
return fmt.Errorf("failed fetching org: %w", err)
100-
}
101-
102-
slug = fullOrg.Organization.RawSlug
103-
} else {
104-
slug = org.Slug
105-
}
106-
107-
// Create new managed Postgres cluster
108-
input := uiex.CreateClusterInput{
109-
Name: pgPlan.AppName,
110-
Region: region.Code,
111-
Plan: "basic", // Default plan for now
112-
OrgSlug: slug,
113-
}
114-
115-
fmt.Fprintf(io.Out, "Provisioning Postgres cluster...\n")
84+
attachToExisting := false
11685

117-
response, err := uiexClient.CreateCluster(ctx, input)
118-
if err != nil {
119-
return fmt.Errorf("failed creating managed postgres cluster: %w", err)
86+
if pgPlan.AppName == "" {
87+
pgPlan.AppName = fmt.Sprintf("%s-db", state.appConfig.AppName)
12088
}
12189

122-
// Wait for cluster to be ready
123-
fmt.Fprintf(io.Out, "Waiting for cluster %s (%s) to be ready...\n", pgPlan.AppName, response.Data.Id)
124-
fmt.Fprintf(io.Out, "If this is taking too long, you can press Ctrl+C to continue with deployment.\n")
125-
fmt.Fprintf(io.Out, "You can check the status later with 'mpg status' and attach with 'mpg attach'.\n")
126-
127-
// Create a separate context for the wait loop that won't propagate cancellation
128-
waitCtx := context.Background()
129-
waitCtx, cancel := context.WithCancel(waitCtx)
130-
defer cancel()
131-
132-
// Channel to signal when cluster is ready
133-
ready := make(chan bool, 1)
134-
errChan := make(chan error, 1)
135-
136-
// Start the wait loop in a goroutine
137-
go func() {
138-
for {
139-
select {
140-
case <-waitCtx.Done():
141-
return
142-
default:
143-
cluster, err := uiexClient.GetManagedClusterById(ctx, response.Data.Id)
144-
if err != nil {
145-
errChan <- fmt.Errorf("failed checking cluster status: %w", err)
146-
return
147-
}
148-
149-
if cluster.Data.Status == "ready" {
150-
ready <- true
151-
return
152-
}
153-
154-
if cluster.Data.Status == "error" {
155-
errChan <- fmt.Errorf("cluster creation failed")
156-
return
157-
}
158-
159-
time.Sleep(5 * time.Second)
90+
if apps, err := apiClient.GetApps(ctx, nil); err == nil {
91+
for _, app := range apps {
92+
if app.Name == pgPlan.AppName {
93+
attachToExisting = true
16094
}
16195
}
162-
}()
163-
164-
// Wait for either ready signal, error, or context cancellation
165-
select {
166-
case <-ready:
167-
// Cluster is ready, continue with user creation
168-
case err := <-errChan:
169-
return err
170-
case <-ctx.Done():
171-
fmt.Fprintf(io.Out, "\nContinuing with deployment. You can check the status later with 'mpg status' and attach with 'mpg attach'.\n")
172-
// Continue with deployment even if cluster isn't ready
173-
return nil
17496
}
17597

176-
// Get the cluster credentials
177-
cluster, err := uiexClient.GetManagedClusterById(ctx, response.Data.Id)
178-
if err != nil {
179-
return fmt.Errorf("failed retrieving cluster credentials: %w", err)
180-
}
98+
if attachToExisting {
99+
// If we try to attach to a PG cluster with the usual username
100+
// format, we'll get an error (since that username already exists)
101+
// by generating a new username with a sufficiently random number
102+
// (in this case, the nanon second that the database is being attached)
103+
currentTime := time.Now().Nanosecond()
104+
dbUser := fmt.Sprintf("%s-%d", pgPlan.AppName, currentTime)
181105

182-
// Set the connection string as a secret
183-
secrets := map[string]string{
184-
"DATABASE_URL": cluster.Credentials.ConnectionUri,
185-
}
106+
err := postgres.AttachCluster(ctx, postgres.AttachParams{
107+
PgAppName: pgPlan.AppName,
108+
AppName: state.Plan.AppName,
109+
DbUser: dbUser,
110+
})
186111

187-
client := flyutil.ClientFromContext(ctx)
188-
if _, err := client.SetSecrets(ctx, state.Plan.AppName, secrets); err != nil {
189-
return fmt.Errorf("failed setting database secrets: %w", err)
112+
if err != nil {
113+
msg := "Failed attaching %s to the Postgres cluster %s: %s.\nTry attaching manually with 'fly postgres attach --app %s %s'\n"
114+
fmt.Fprintf(io.Out, msg, state.Plan.AppName, pgPlan.AppName, err, state.Plan.AppName, pgPlan.AppName)
115+
return err
116+
} else {
117+
fmt.Fprintf(io.Out, "Postgres cluster %s is now attached to %s\n", pgPlan.AppName, state.Plan.AppName)
118+
}
119+
} else {
120+
// Create new PG cluster
121+
org, err := state.Org(ctx)
122+
if err != nil {
123+
return err
124+
}
125+
region, err := state.Region(ctx)
126+
if err != nil {
127+
return err
128+
}
129+
err = postgres.CreateCluster(ctx, org, &region, &postgres.ClusterParams{
130+
PostgresConfiguration: postgres.PostgresConfiguration{
131+
Name: pgPlan.AppName,
132+
DiskGb: pgPlan.DiskSizeGB,
133+
InitialClusterSize: pgPlan.Nodes,
134+
VMSize: pgPlan.VmSize,
135+
MemoryMb: pgPlan.VmRam,
136+
},
137+
ScaleToZero: &pgPlan.AutoStop,
138+
Autostart: true, // TODO(Ali): Do we want this?
139+
Manager: flypg.ReplicationManager,
140+
})
141+
if err != nil {
142+
fmt.Fprintf(io.Out, "Failed creating the Postgres cluster %s: %s\n", pgPlan.AppName, err)
143+
} else {
144+
err = postgres.AttachCluster(ctx, postgres.AttachParams{
145+
PgAppName: pgPlan.AppName,
146+
AppName: state.Plan.AppName,
147+
SuperUser: true,
148+
})
149+
150+
if err != nil {
151+
msg := "Failed attaching %s to the Postgres cluster %s: %s.\nTry attaching manually with 'fly postgres attach --app %s %s'\n"
152+
fmt.Fprintf(io.Out, msg, state.Plan.AppName, pgPlan.AppName, err, state.Plan.AppName, pgPlan.AppName)
153+
} else {
154+
fmt.Fprintf(io.Out, "Postgres cluster %s is now attached to %s\n", pgPlan.AppName, state.Plan.AppName)
155+
}
156+
}
157+
if err != nil {
158+
const msg = "Error creating Postgres database. Be warned that this may affect deploys"
159+
fmt.Fprintln(io.Out, io.ColorScheme().Red(msg))
160+
}
190161
}
191162

192-
fmt.Fprintf(io.Out, "Postgres cluster %s is ready and attached to %s\n", response.Data.Id, state.Plan.AppName)
193-
fmt.Fprintf(io.Out, "The following secret was added to %s:\n DATABASE_URL=%s\n", state.Plan.AppName, cluster.Credentials.ConnectionUri)
194-
195163
return nil
196164
}
197165

internal/command/launch/plan/postgres.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,18 @@ func (p *PostgresPlan) Provider() any {
2626
return nil
2727
}
2828

29-
func DefaultPostgres(plan *LaunchPlan) PostgresPlan {
29+
func DefaultPostgres(plan *LaunchPlan, mpgEnabled bool) PostgresPlan {
30+
var vmRam, diskSizeGb, price int
31+
if mpgEnabled {
32+
vmRam = 1024 // 1GB RAM for basic plan
33+
diskSizeGb = 10
34+
price = 38
35+
} else {
36+
vmRam = 256
37+
diskSizeGb = 1
38+
price = -1
39+
}
40+
3041
return PostgresPlan{
3142
// TODO: Once supabase is GA, we want to default to Supabase
3243
FlyPostgres: &FlyPostgresPlan{
@@ -36,10 +47,10 @@ func DefaultPostgres(plan *LaunchPlan) PostgresPlan {
3647
// so it constructs the name on-the-spot each time it needs it)
3748
AppName: plan.AppName + "-db",
3849
VmSize: "shared-cpu-1x",
39-
VmRam: 1024, // 1GB RAM for basic plan
50+
VmRam: vmRam,
4051
Nodes: 1,
41-
DiskSizeGB: 10,
42-
Price: 38,
52+
DiskSizeGB: diskSizeGb,
53+
Price: price,
4354
},
4455
}
4556
}

internal/command/launch/plan_builder.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/superfly/flyctl/internal/flyerr"
2323
"github.com/superfly/flyctl/internal/flyutil"
2424
"github.com/superfly/flyctl/internal/haikunator"
25+
"github.com/superfly/flyctl/internal/launchdarkly"
2526
"github.com/superfly/flyctl/internal/prompt"
2627
"github.com/superfly/flyctl/iostreams"
2728
"github.com/superfly/flyctl/scanner"
@@ -224,12 +225,18 @@ func buildManifest(ctx context.Context, parentConfig *appconfig.Config, recovera
224225
}
225226

226227
if srcInfo != nil {
228+
ldClient, err := launchdarkly.NewServiceClient()
229+
if err != nil {
230+
return nil, nil, err
231+
}
232+
mpgEnabled := ldClient.ManagedPostgresEnabled()
233+
227234
lp.ScannerFamily = srcInfo.Family
228235
const scannerSource = "determined from app source"
229236
if !flag.GetBool(ctx, "no-db") {
230237
switch srcInfo.DatabaseDesired {
231238
case scanner.DatabaseKindPostgres:
232-
lp.Postgres = plan.DefaultPostgres(lp)
239+
lp.Postgres = plan.DefaultPostgres(lp, mpgEnabled)
233240
planSource.postgresSource = scannerSource
234241
case scanner.DatabaseKindMySQL:
235242
// TODO
@@ -239,7 +246,7 @@ func buildManifest(ctx context.Context, parentConfig *appconfig.Config, recovera
239246
}
240247
// Force Postgres provisioning if --db flag is set
241248
if flag.GetBool(ctx, "db") {
242-
lp.Postgres = plan.DefaultPostgres(lp)
249+
lp.Postgres = plan.DefaultPostgres(lp, mpgEnabled)
243250
planSource.postgresSource = "forced by --db flag"
244251
}
245252
if !flag.GetBool(ctx, "no-redis") && srcInfo.RedisDesired {

internal/launchdarkly/launchdarkly.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ func NewClient(ctx context.Context, userInfo UserInfo) (*Client, error) {
7676
return ldClient, nil
7777
}
7878

79+
func NewServiceClient() (*Client, error) {
80+
ctx := context.Background()
81+
_, span := tracing.GetTracer().Start(ctx, "new_flyctl_feature_flag_client")
82+
defer span.End()
83+
84+
ldClient := &Client{ldContext: ldcontext.NewWithKind(ldcontext.Kind("service"), "flyctl"), flagsMutex: sync.Mutex{}}
85+
86+
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
87+
defer cancel()
88+
// we don't really care if this errors or not, but it's good to at least try
89+
_ = ldClient.updateFeatureFlags(timeoutCtx)
90+
91+
go ldClient.monitor(ctx)
92+
return ldClient, nil
93+
}
94+
7995
func (ldClient *Client) monitor(ctx context.Context) {
8096
logger := logger.MaybeFromContext(ctx)
8197

@@ -177,3 +193,17 @@ func (ldClient *Client) updateFeatureFlags(ctx context.Context) error {
177193

178194
return nil
179195
}
196+
197+
func (ldClient *Client) ManagedPostgresEnabled() bool {
198+
choice := ldClient.getLaunchPostgresChoiceFlag()
199+
return choice == "mpg" || choice == "both"
200+
}
201+
202+
func (ldClient *Client) UnmanagedPostgresEnabled() bool {
203+
choice := ldClient.getLaunchPostgresChoiceFlag()
204+
return choice == "unmanaged-pg" || choice == "both"
205+
}
206+
207+
func (ldClient *Client) getLaunchPostgresChoiceFlag() string {
208+
return ldClient.GetFeatureFlagValue("launch-postgres-choice", "unmanaged-pg").(string)
209+
}

0 commit comments

Comments
 (0)