Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support SQL User Management #174

Merged
merged 21 commits into from
May 6, 2024
2 changes: 1 addition & 1 deletion internal/cli/serverless/backup/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func DeleteCmd(h *internal.Helper) *cobra.Command {
}

if userInput != confirmed {
return errors.New("incorrect confirm string entered, skipping branch deletion")
return errors.New("incorrect confirm string entered, skipping buckup deletion")
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
3 changes: 3 additions & 0 deletions internal/cli/serverless/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"tidbcloud-cli/internal/cli/serverless/branch"
"tidbcloud-cli/internal/cli/serverless/dataimport"
"tidbcloud-cli/internal/cli/serverless/export"
"tidbcloud-cli/internal/cli/serverless/sqluser"

"github.com/spf13/cobra"
)
Expand All @@ -43,5 +44,7 @@ func Cmd(h *internal.Helper) *cobra.Command {
serverlessCmd.AddCommand(export.Cmd(h))
serverlessCmd.AddCommand(SpendingLimitCmd(h))
serverlessCmd.AddCommand(RegionCmd(h))

serverlessCmd.AddCommand(sqluser.SQLUserCmd(h))
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
return serverlessCmd
}
302 changes: 302 additions & 0 deletions internal/cli/serverless/sqluser/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
// Copyright 2024 PingCAP, Inc.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sqluser

import (
"context"
"fmt"
"strings"
"time"

"tidbcloud-cli/internal"
"tidbcloud-cli/internal/config"
"tidbcloud-cli/internal/flag"
"tidbcloud-cli/internal/service/cloud"
"tidbcloud-cli/internal/telemetry"
"tidbcloud-cli/internal/ui"
"tidbcloud-cli/internal/util"

iamApi "tidbcloud-cli/pkg/tidbcloud/v1beta1/iam/client/account"
iamModel "tidbcloud-cli/pkg/tidbcloud/v1beta1/iam/models"
serverlessApi "tidbcloud-cli/pkg/tidbcloud/v1beta1/serverless/client/serverless_service"

"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/fatih/color"
"github.com/juju/errors"
"github.com/spf13/cobra"
)

type CreateOpts struct {
interactive bool
}

const (
WaitInterval = 5 * time.Second
WaitTimeout = 2 * time.Minute
)

var createSQLUserField = map[string]int{
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
flag.UserName: 0,
flag.Password: 1,
}

func (c CreateOpts) NonInteractiveFlags() []string {
return []string{
flag.ClusterID,
flag.UserName,
flag.Password,
flag.UserRole,
}
}

func (c CreateOpts) RequiredFlags() []string {
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
return []string{
flag.ClusterID,
flag.UserName,
flag.Password,
flag.UserRole,
}
}

func CreateCmd(h *internal.Helper) *cobra.Command {
opts := CreateOpts{
interactive: true,
}

var CreateCmd = &cobra.Command{
Use: "create",
Short: "Create a SQL user",
Aliases: []string{"c"},
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
Annotations: make(map[string]string),
Example: fmt.Sprintf(` Create a SQL user in interactive mode:
$ %[1]s serverless sql-user create

Create a TiDB Serverless SQL user in non-interactive mode:
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
$ %[1]s serverless sql-user create --user <user-name> --password <password> --role <role> --cluster-id <cluster-id>`,
config.CliName),
PreRunE: func(cmd *cobra.Command, args []string) error {
flags := opts.NonInteractiveFlags()
for _, fn := range flags {
f := cmd.Flags().Lookup(fn)
if f != nil && f.Changed {
opts.interactive = false
}
}

// mark required flags in non-interactive mode
if !opts.interactive {
for _, fn := range opts.RequiredFlags() {
err := cmd.MarkFlagRequired(fn)
if err != nil {
return errors.Trace(err)
}
}
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
d, err := h.Client()
if err != nil {
return err
}

var clusterID string
var userName string
var password string
var userRole string
var userPrefix string
var customRoles []string
if opts.interactive {
cmd.Annotations[telemetry.InteractiveMode] = "true"
if !h.IOStreams.CanPrompt {
return errors.New("The terminal doesn't support interactive mode, please use non-interactive mode")
}

// interactive mode
project, err := cloud.GetSelectedProject(ctx, h.QueryPageSize, d)
if err != nil {
return err
}
projectID := project.ID

cluster, err := cloud.GetSelectedCluster(ctx, projectID, h.QueryPageSize, d)
if err != nil {
return err
}
clusterID = cluster.ID
userPrefix, err = GetUserPrefix(ctx, d, clusterID)
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return errors.Trace(err)
}

userRole, err = cloud.GetSelectedBuiltinRole()
if err != nil {
return err
}

// variables for input
fmt.Fprintln(h.IOStreams.Out, color.BlueString("Please input the following options"))
zhangyangyu marked this conversation as resolved.
Show resolved Hide resolved

p := tea.NewProgram(initialCreateInputModel(userPrefix))
inputModel, err := p.Run()
if err != nil {
return errors.Trace(err)
}
if inputModel.(ui.TextInputModel).Interrupted {
return util.InterruptError
}

userName = inputModel.(ui.TextInputModel).Inputs[createSQLUserField[flag.UserName]].Value()
password = inputModel.(ui.TextInputModel).Inputs[createSQLUserField[flag.Password]].Value()

} else {
// non-interactive mode doesn't need projectID
cID, err := cmd.Flags().GetString(flag.ClusterID)
if err != nil {
return errors.Trace(err)
}
clusterID = cID

userPrefix, err = GetUserPrefix(ctx, d, clusterID)
if err != nil {
return errors.Trace(err)
}

uName, err := cmd.Flags().GetString(flag.UserName)
if err != nil {
return errors.Trace(err)
}
userName = uName

pw, err := cmd.Flags().GetString(flag.Password)
if err != nil {
return errors.Trace(err)
}
password = pw

uRole, err := cmd.Flags().GetString(flag.UserRole)
if err != nil {
return errors.Trace(err)
}
userRole = uRole
}

// generate the built-in role
builtinRole, customRoles := GetBuiltinRoleAndCustomRoles(userRole)

params := iamApi.NewPostV1beta1ClustersClusterIDSQLUsersParams().
WithClusterID(clusterID).
WithSQLUser(&iamModel.APICreateSQLUserReq{
AuthMethod: util.MYSQLNATIVEPASSWORD,
UserName: userName,
Password: password,
BuiltinRole: builtinRole,
CustomRoles: customRoles,
}).
WithContext(ctx)

_, err = d.CreateSQLUser(params)
if err != nil {
return errors.Trace(err)
}

_, err = fmt.Fprintln(h.IOStreams.Out, color.GreenString("SQL user %s.%s is created", userPrefix, userName))
if err != nil {
return err
}
return nil

},
}

CreateCmd.Flags().StringP(flag.ClusterID, flag.ClusterIDShort, "", "The ID of the cluster.")
CreateCmd.Flags().StringP(flag.UserName, "", "", "The name of the SQL user.")
CreateCmd.Flags().StringP(flag.Password, "", "", "The password of the SQL user.")
CreateCmd.Flags().StringP(flag.UserRole, "", "", "The built-in role of the SQL user.")
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved

return CreateCmd
}

// func GetBuiltinRole(userRole string) string {
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
// role := strings.ToLower(userRole)
// switch role {
// case util.ADMIN:
// role = util.ADMIN_ROLE
// case util.READWRITE:
// role = util.READWRITE_ROLE
// case util.READONLY:
// role = util.READONLY_ROLE
// default:
// role = userRole
// }

// return role
// }

func initialCreateInputModel(userPrefix string) ui.TextInputModel {
m := ui.TextInputModel{
Inputs: make([]textinput.Model, len(createSQLUserField)),
}

for k, v := range createSQLUserField {
t := textinput.New()
t.Cursor.Style = config.CursorStyle
t.CharLimit = 32

switch k {
case flag.UserName:
// add a prefix showing the user prefix
t.Placeholder = "User Name"
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
t.Focus()
t.PromptStyle = config.FocusedStyle
t.TextStyle = config.FocusedStyle
t.Prompt = userPrefix + "."
case flag.Password:
t.Placeholder = "Password"
t.EchoMode = textinput.EchoPassword
t.EchoCharacter = '•'
}
m.Inputs[v] = t
}
return m
}

func GetUserPrefix(ctx context.Context, d cloud.TiDBCloudClient, clusterID string) (string, error) {
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
params := serverlessApi.NewServerlessServiceGetClusterParams().WithClusterID(clusterID).WithContext(ctx)

cluster, err := d.GetCluster(params)
if err != nil {
return "", errors.Trace(err)
}

return cluster.Payload.UserPrefix, nil
}

func GetBuiltinRoleAndCustomRoles(userRole string) (string, []string) {
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
lowerRole := strings.ToLower(userRole)
switch lowerRole {
case util.ADMIN:
FingerLeader marked this conversation as resolved.
Show resolved Hide resolved
return util.ADMIN_ROLE, nil
case util.READWRITE:
return util.READWRITE_ROLE, nil
case util.READONLY:
return util.READONLY_ROLE, nil
default:
roles := strings.Split(userRole, ",")
return "", roles
}
}
Loading
Loading