Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/planetscale/planetscale-go v0.142.0
github.com/planetscale/planetscale-go v0.143.0
github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4
github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7
github.com/spf13/cobra v1.10.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e h1:MZ8D+Z3m2vvqGZLvoQfpaGg/j1fNDr4j03s3PRz4rVY=
github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e/go.mod h1:hwAsSPQdvPa3WcfKfzTXxtEq/HlqwLjQasfO6QbGo4Q=
github.com/planetscale/planetscale-go v0.142.0 h1:luApvfbD2xcg8gS9TmUC7nFINl4t3T9qzX7Ed8rYlwQ=
github.com/planetscale/planetscale-go v0.142.0/go.mod h1:PheYDHAwF14wfCBak1M0J64AdPW8NUeyvgPgWqe7zpI=
github.com/planetscale/planetscale-go v0.143.0 h1:3LeJXrPYIkXdxUcGIK5rhitA44D6HMp9ZxmVTy7ozO0=
github.com/planetscale/planetscale-go v0.143.0/go.mod h1:PheYDHAwF14wfCBak1M0J64AdPW8NUeyvgPgWqe7zpI=
github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4 h1:Xv5pj20Rhfty1Tv0OVcidg4ez4PvGrpKvb6rvUwQgDs=
github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4/go.mod h1:M52h5IWxAcbdQ1hSZrLAGQC4ZXslxEsK/Wh9nu3wdWs=
github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7 h1:aRd6vdE1fyuSI4RVj7oCr8lFmgqXvpnPUmN85VbZCp8=
Expand Down
120 changes: 120 additions & 0 deletions internal/cmd/role/reassign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package role

import (
"errors"
"fmt"
"os"

"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/planetscale/cli/internal/cmdutil"
"github.com/planetscale/cli/internal/printer"
ps "github.com/planetscale/planetscale-go/planetscale"

"github.com/spf13/cobra"
)

func ReassignCmd(ch *cmdutil.Helper) *cobra.Command {
var flags struct {
force bool
successor string
}

cmd := &cobra.Command{
Use: "reassign <database> <branch> <role-id>",
Short: "Reassign objects owned by a role to another role",
Args: cmdutil.RequiredArgs("database", "branch", "role-id"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
database := args[0]
branch := args[1]
roleID := args[2]

if flags.successor == "" {
return fmt.Errorf("--successor flag is required")
}

client, err := ch.Client()
if err != nil {
return err
}

if !flags.force {
if ch.Printer.Format() != printer.Human {
return fmt.Errorf("cannot reassign role objects with the output format %q (run with --force to override)", ch.Printer.Format())
}

confirmationName := fmt.Sprintf("%s/%s/%s", database, branch, roleID)
if !printer.IsTTY {
return fmt.Errorf("cannot confirm object reassignment for role %q (run with --force to override)", confirmationName)
}

confirmationMessage := fmt.Sprintf("%s %s %s", printer.Bold("Please type"),
printer.BoldBlue(confirmationName), printer.Bold("to confirm:"))

prompt := &survey.Input{
Message: confirmationMessage,
}

var userInput string
err := survey.AskOne(prompt, &userInput)
if err != nil {
if err == terminal.InterruptErr {
os.Exit(0)
} else {
return err
}
}

// If the confirmations don't match up, let's return an error.
if userInput != confirmationName {
return errors.New("incorrect role identifier entered, skipping object reassignment")
}
}

end := ch.Printer.PrintProgress(fmt.Sprintf("Reassigning objects from role %s to %s in %s/%s...",
printer.BoldBlue(roleID), printer.BoldBlue(flags.successor), printer.BoldBlue(database), printer.BoldBlue(branch)))
defer end()

err = client.PostgresRoles.ReassignObjects(ctx, &ps.ReassignPostgresRoleObjectsRequest{
Organization: ch.Config.Organization,
Database: database,
Branch: branch,
RoleId: roleID,
Successor: flags.successor,
})
if err != nil {
switch cmdutil.ErrCode(err) {
case ps.ErrNotFound:
return fmt.Errorf("role %s does not exist in branch %s of database %s (organization: %s)",
printer.BoldBlue(roleID), printer.BoldBlue(branch), printer.BoldBlue(database), printer.BoldBlue(ch.Config.Organization))
default:
return cmdutil.HandleError(err)
}
}

end()

if ch.Printer.Format() == printer.Human {
ch.Printer.Printf("Objects owned by role %s were successfully reassigned to %s in %s/%s.\n",
printer.BoldBlue(roleID), printer.BoldBlue(flags.successor), printer.BoldBlue(database), printer.BoldBlue(branch))
return nil
}

return ch.Printer.PrintResource(
map[string]string{
"result": "objects reassigned",
"role_id": roleID,
"successor": flags.successor,
"branch": branch,
},
)
},
}

cmd.Flags().BoolVar(&flags.force, "force", false, "Reassign objects without confirmation")
cmd.Flags().StringVar(&flags.successor, "successor", "", "Role to transfer ownership to (required)")
cmd.MarkFlagRequired("successor") // nolint:errcheck

return cmd
}
104 changes: 104 additions & 0 deletions internal/cmd/role/reset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package role

import (
"errors"
"fmt"
"os"

"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/planetscale/cli/internal/cmdutil"
"github.com/planetscale/cli/internal/printer"
ps "github.com/planetscale/planetscale-go/planetscale"

"github.com/spf13/cobra"
)

func ResetCmd(ch *cmdutil.Helper) *cobra.Command {
var flags struct {
force bool
}

cmd := &cobra.Command{
Use: "reset <database> <branch> <role-id>",
Short: "Reset a role's password",
Args: cmdutil.RequiredArgs("database", "branch", "role-id"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
database := args[0]
branch := args[1]
roleID := args[2]

client, err := ch.Client()
if err != nil {
return err
}

if !flags.force {
if ch.Printer.Format() != printer.Human {
return fmt.Errorf("cannot reset role password with the output format %q (run with --force to override)", ch.Printer.Format())
}

confirmationName := fmt.Sprintf("%s/%s/%s", database, branch, roleID)
if !printer.IsTTY {
return fmt.Errorf("cannot confirm password reset for role %q (run with --force to override)", confirmationName)
}

confirmationMessage := fmt.Sprintf("%s %s %s", printer.Bold("Please type"),
printer.BoldBlue(confirmationName), printer.Bold("to confirm:"))

prompt := &survey.Input{
Message: confirmationMessage,
}

var userInput string
err := survey.AskOne(prompt, &userInput)
if err != nil {
if err == terminal.InterruptErr {
os.Exit(0)
} else {
return err
}
}

// If the confirmations don't match up, let's return an error.
if userInput != confirmationName {
return errors.New("incorrect role identifier entered, skipping password reset")
}
}

end := ch.Printer.PrintProgress(fmt.Sprintf("Resetting password for role %s in %s/%s...",
printer.BoldBlue(roleID), printer.BoldBlue(database), printer.BoldBlue(branch)))
defer end()

role, err := client.PostgresRoles.ResetPassword(ctx, &ps.ResetPostgresRolePasswordRequest{
Organization: ch.Config.Organization,
Database: database,
Branch: branch,
RoleId: roleID,
})
if err != nil {
switch cmdutil.ErrCode(err) {
case ps.ErrNotFound:
return fmt.Errorf("role %s does not exist in branch %s of database %s (organization: %s)",
printer.BoldBlue(roleID), printer.BoldBlue(branch), printer.BoldBlue(database), printer.BoldBlue(ch.Config.Organization))
default:
return cmdutil.HandleError(err)
}
}

end()

if ch.Printer.Format() == printer.Human {
ch.Printer.Printf("Password for role %s was successfully reset in %s/%s.\n",
printer.BoldBlue(roleID), printer.BoldBlue(database), printer.BoldBlue(branch))
}

return ch.Printer.PrintResource(toPostgresRole(role))
},
}

cmd.Flags().BoolVar(&flags.force, "force", false, "Reset password without confirmation")

return cmd
}
2 changes: 2 additions & 0 deletions internal/cmd/role/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ func RoleCmd(ch *cmdutil.Helper) *cobra.Command {
DeleteCmd(ch),
GetCmd(ch),
ListCmd(ch),
ReassignCmd(ch),
RenewCmd(ch),
ResetCmd(ch),
ResetDefaultCmd(ch),
UpdateCmd(ch),
)
Expand Down
14 changes: 14 additions & 0 deletions internal/mock/postgres_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ type PostgresRolesService struct {
DeleteFnInvoked bool
ResetDefaultRoleFn func(context.Context, *ps.ResetDefaultRoleRequest) (*ps.PostgresRole, error)
ResetDefaultRoleFnInvoked bool
ResetPasswordFn func(context.Context, *ps.ResetPostgresRolePasswordRequest) (*ps.PostgresRole, error)
ResetPasswordFnInvoked bool
ReassignObjectsFn func(context.Context, *ps.ReassignPostgresRoleObjectsRequest) error
ReassignObjectsFnInvoked bool
}

func (s *PostgresRolesService) List(ctx context.Context, req *ps.ListPostgresRolesRequest, opts ...ps.ListOption) ([]*ps.PostgresRole, error) {
Expand Down Expand Up @@ -57,3 +61,13 @@ func (s *PostgresRolesService) ResetDefaultRole(ctx context.Context, req *ps.Res
s.ResetDefaultRoleFnInvoked = true
return s.ResetDefaultRoleFn(ctx, req)
}

func (s *PostgresRolesService) ResetPassword(ctx context.Context, req *ps.ResetPostgresRolePasswordRequest) (*ps.PostgresRole, error) {
s.ResetPasswordFnInvoked = true
return s.ResetPasswordFn(ctx, req)
}

func (s *PostgresRolesService) ReassignObjects(ctx context.Context, req *ps.ReassignPostgresRoleObjectsRequest) error {
s.ReassignObjectsFnInvoked = true
return s.ReassignObjectsFn(ctx, req)
}