Skip to content
Open
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
8 changes: 8 additions & 0 deletions internal/command/launch/plan/postgres_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ func (m *mockUIEXClient) RestoreManagedClusterBackup(ctx context.Context, cluste
return uiex.RestoreManagedClusterBackupResponse{}, nil
}

func (m *mockUIEXClient) CreateAttachment(ctx context.Context, clusterId string, input uiex.CreateAttachmentInput) (uiex.CreateAttachmentResponse, error) {
return uiex.CreateAttachmentResponse{}, nil
}

func (m *mockUIEXClient) DeleteAttachment(ctx context.Context, clusterId string, appName string) (uiex.DeleteAttachmentResponse, error) {
return uiex.DeleteAttachmentResponse{}, nil
}

func (m *mockUIEXClient) CreateBuild(ctx context.Context, in uiex.CreateBuildRequest) (*uiex.BuildResponse, error) {
return &uiex.BuildResponse{}, nil
}
Expand Down
9 changes: 9 additions & 0 deletions internal/command/mpg/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,15 @@ func runAttach(ctx context.Context) error {
return err
}

// Create attachment record to track the cluster-app relationship
attachInput := uiex.CreateAttachmentInput{
AppName: appName,
}
if _, err := uiexClient.CreateAttachment(ctx, cluster.Id, attachInput); err != nil {
// Log warning but don't fail - the secret was set successfully
fmt.Fprintf(io.ErrOut, "Warning: failed to create attachment record: %v\n", err)
}

fmt.Fprintf(io.Out, "\nPostgres cluster %s is being attached to %s\n", cluster.Id, appName)
fmt.Fprintf(io.Out, "The following secret was added to %s:\n %s=%s\n", appName, variableName, connectionUri)

Expand Down
90 changes: 90 additions & 0 deletions internal/command/mpg/detach.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package mpg

import (
"context"
"fmt"

"github.com/spf13/cobra"
"github.com/superfly/flyctl/internal/appconfig"
"github.com/superfly/flyctl/internal/command"
"github.com/superfly/flyctl/internal/flag"
"github.com/superfly/flyctl/internal/flyutil"
"github.com/superfly/flyctl/internal/uiexutil"
"github.com/superfly/flyctl/iostreams"
)

func newDetach() *cobra.Command {
const (
short = "Detach a managed Postgres cluster from an app"
long = short + ". " +
`This command will remove the attachment record linking the app to the cluster.
Note: This does NOT remove any secrets from the app. Use 'fly secrets unset' to remove secrets.`
usage = "detach <CLUSTER ID>"
)

cmd := command.New(usage, short, long, runDetach,
command.RequireSession,
command.RequireAppName,
)
cmd.Args = cobra.MaximumNArgs(1)

flag.Add(cmd,
flag.App(),
flag.AppConfig(),
)

return cmd
}

func runDetach(ctx context.Context) error {
// Check token compatibility early
if err := validateMPGTokenCompatibility(ctx); err != nil {
return err
}

var (
clusterId = flag.FirstArg(ctx)
appName = appconfig.NameFromContext(ctx)
client = flyutil.ClientFromContext(ctx)
io = iostreams.FromContext(ctx)
)

// Get app details to determine which org it belongs to
app, err := client.GetAppBasic(ctx, appName)
if err != nil {
return fmt.Errorf("failed retrieving app %s: %w", appName, err)
}

appOrgSlug := app.Organization.RawSlug
if appOrgSlug != "" && clusterId == "" {
fmt.Fprintf(io.Out, "Listing clusters in organization %s\n", appOrgSlug)
}

// Get cluster details
cluster, _, err := ClusterFromArgOrSelect(ctx, clusterId, appOrgSlug)
if err != nil {
return fmt.Errorf("failed retrieving cluster %s: %w", clusterId, err)
}

clusterOrgSlug := cluster.Organization.Slug

// Verify that the app and cluster are in the same organization
if appOrgSlug != clusterOrgSlug {
return fmt.Errorf("app %s is in organization %s, but cluster %s is in organization %s. They must be in the same organization",
appName, appOrgSlug, cluster.Id, clusterOrgSlug)
}

uiexClient := uiexutil.ClientFromContext(ctx)

// Delete the attachment record
_, err = uiexClient.DeleteAttachment(ctx, cluster.Id, appName)
if err != nil {
return fmt.Errorf("failed to detach: %w", err)
}

fmt.Fprintf(io.Out, "\nPostgres cluster %s has been detached from %s\n", cluster.Id, appName)
fmt.Fprintf(io.Out, "Note: This only removes the attachment record. Any secrets (like DATABASE_URL) are still set on the app.\n")
fmt.Fprintf(io.Out, "Use 'fly secrets unset DATABASE_URL -a %s' to remove the connection string.\n", appName)

return nil
}
18 changes: 17 additions & 1 deletion internal/command/mpg/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package mpg
import (
"context"
"fmt"
"strings"

"github.com/spf13/cobra"

"github.com/superfly/flyctl/gql"
"github.com/superfly/flyctl/internal/uiex"
"github.com/superfly/flyctl/iostreams"

"github.com/superfly/flyctl/internal/command"
Expand Down Expand Up @@ -95,8 +97,22 @@ func runList(ctx context.Context) error {
cluster.Region,
cluster.Status,
cluster.Plan,
formatAttachedApps(cluster.AttachedApps),
})
}

return render.Table(out, "", rows, "ID", "Name", "Org", "Region", "Status", "Plan")
return render.Table(out, "", rows, "ID", "Name", "Org", "Region", "Status", "Plan", "Attached Apps")
}

// formatAttachedApps formats the list of attached apps for display
func formatAttachedApps(apps []uiex.AttachedApp) string {
if len(apps) == 0 {
return "<no attached apps>"
}

names := make([]string, len(apps))
for i, app := range apps {
names[i] = app.Name
}
return strings.Join(names, ", ")
}
1 change: 1 addition & 0 deletions internal/command/mpg/mpg.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func New() *cobra.Command {
newProxy(),
newConnect(),
newAttach(),
newDetach(),
newStatus(),
newList(),
newCreate(),
Expand Down
Loading
Loading