@@ -3,6 +3,7 @@ package mpg
33import (
44 "context"
55 "fmt"
6+ "net/url"
67
78 "github.com/spf13/cobra"
89 "github.com/superfly/flyctl/internal/appconfig"
@@ -11,6 +12,8 @@ import (
1112 "github.com/superfly/flyctl/internal/flag"
1213 "github.com/superfly/flyctl/internal/flapsutil"
1314 "github.com/superfly/flyctl/internal/flyutil"
15+ "github.com/superfly/flyctl/internal/prompt"
16+ "github.com/superfly/flyctl/internal/uiex"
1417 "github.com/superfly/flyctl/internal/uiexutil"
1518 "github.com/superfly/flyctl/iostreams"
1619)
@@ -40,6 +43,16 @@ func newAttach() *cobra.Command {
4043 Default : "DATABASE_URL" ,
4144 Description : "The name of the environment variable that will be added to the attached app" ,
4245 },
46+ flag.String {
47+ Name : "database" ,
48+ Shorthand : "d" ,
49+ Description : "The database to connect to" ,
50+ },
51+ flag.String {
52+ Name : "username" ,
53+ Shorthand : "u" ,
54+ Description : "The username to connect as" ,
55+ },
4356 )
4457
4558 return cmd
@@ -65,6 +78,9 @@ func runAttach(ctx context.Context) error {
6578 }
6679
6780 appOrgSlug := app .Organization .RawSlug
81+ if appOrgSlug != "" && clusterId == "" {
82+ fmt .Fprintf (io .Out , "Listing clusters in organization %s\n " , appOrgSlug )
83+ }
6884
6985 // Get cluster details to determine which org it belongs to
7086 cluster , _ , err := ClusterFromArgOrSelect (ctx , clusterId , appOrgSlug )
@@ -82,11 +98,87 @@ func runAttach(ctx context.Context) error {
8298
8399 uiexClient := uiexutil .ClientFromContext (ctx )
84100
101+ // Username selection: flag > prompt (if interactive) > empty (use default credentials)
102+ username := flag .GetString (ctx , "username" )
103+ if username == "" && io .IsInteractive () {
104+ // Prompt for user selection
105+ usersResponse , err := uiexClient .ListUsers (ctx , cluster .Id )
106+ if err != nil {
107+ return fmt .Errorf ("failed to list users: %w" , err )
108+ }
109+
110+ if len (usersResponse .Data ) > 0 {
111+ var userOptions []string
112+ for _ , user := range usersResponse .Data {
113+ userOptions = append (userOptions , fmt .Sprintf ("%s [%s]" , user .Name , user .Role ))
114+ }
115+
116+ var userIndex int
117+ err = prompt .Select (ctx , & userIndex , "Select user:" , "" , userOptions ... )
118+ if err != nil {
119+ return err
120+ }
121+
122+ username = usersResponse .Data [userIndex ].Name
123+ }
124+ // If no users found, username remains empty and will use default credentials
125+ }
126+
127+ // Database selection priority: flag > prompt result (if interactive) > credentials.DBName
128+ var db string
129+ if database := flag .GetString (ctx , "database" ); database != "" {
130+ db = database
131+ } else if io .IsInteractive () {
132+ // Prompt for database selection
133+ databasesResponse , err := uiexClient .ListDatabases (ctx , cluster .Id )
134+ if err != nil {
135+ return fmt .Errorf ("failed to list databases: %w" , err )
136+ }
137+
138+ if len (databasesResponse .Data ) > 0 {
139+ var dbOptions []string
140+ for _ , database := range databasesResponse .Data {
141+ dbOptions = append (dbOptions , database .Name )
142+ }
143+
144+ var dbIndex int
145+ err = prompt .Select (ctx , & dbIndex , "Select database:" , "" , dbOptions ... )
146+ if err != nil {
147+ return err
148+ }
149+
150+ db = databasesResponse .Data [dbIndex ].Name
151+ }
152+ }
153+
154+ // Get cluster details with credentials
85155 response , err := uiexClient .GetManagedClusterById (ctx , cluster .Id )
86156 if err != nil {
87157 return fmt .Errorf ("failed retrieving cluster %s: %w" , clusterId , err )
88158 }
89159
160+ // Get credentials - use user-specific endpoint if username provided, otherwise use default
161+ var credentials uiex.GetManagedClusterCredentialsResponse
162+ if username != "" {
163+ userCreds , err := uiexClient .GetUserCredentials (ctx , cluster .Id , username )
164+ if err != nil {
165+ return fmt .Errorf ("failed retrieving credentials for user %s: %w" , username , err )
166+ }
167+ // Convert user credentials to the standard format
168+ credentials = uiex.GetManagedClusterCredentialsResponse {
169+ User : userCreds .Data .User ,
170+ Password : userCreds .Data .Password ,
171+ DBName : response .Credentials .DBName , // Use default DB name from cluster credentials
172+ }
173+ } else {
174+ credentials = response .Credentials
175+ }
176+
177+ // Use selected database or fall back to default from credentials
178+ if db == "" {
179+ db = credentials .DBName
180+ }
181+
90182 ctx , flapsClient , _ , err := flapsutil .SetClient (ctx , nil , appName )
91183 if err != nil {
92184 return err
@@ -110,15 +202,28 @@ func runAttach(ctx context.Context) error {
110202 }
111203 }
112204
205+ // Build connection URI with selected user and database
206+ // Parse the base connection URI to extract host/port
207+ baseUri := response .Credentials .ConnectionUri
208+ parsedUri , err := url .Parse (baseUri )
209+ if err != nil {
210+ return fmt .Errorf ("failed to parse connection URI: %w" , err )
211+ }
212+
213+ // Build new connection URI with selected user, password, and database
214+ parsedUri .User = url .UserPassword (credentials .User , credentials .Password )
215+ parsedUri .Path = "/" + db
216+ connectionUri := parsedUri .String ()
217+
113218 s := map [string ]string {}
114- s [variableName ] = response . Credentials . ConnectionUri
219+ s [variableName ] = connectionUri
115220
116221 if err := appsecrets .Update (ctx , flapsClient , app .Name , s , nil ); err != nil {
117222 return err
118223 }
119224
120- fmt .Fprintf (io .Out , "\n Postgres cluster %s is being attached to %s\n " , clusterId , appName )
121- fmt .Fprintf (io .Out , "The following secret was added to %s:\n %s=%s\n " , appName , variableName , response . Credentials . ConnectionUri )
225+ fmt .Fprintf (io .Out , "\n Postgres cluster %s is being attached to %s\n " , cluster . Id , appName )
226+ fmt .Fprintf (io .Out , "The following secret was added to %s:\n %s=%s\n " , appName , variableName , connectionUri )
122227
123228 return nil
124229}
0 commit comments