diff --git a/README.md b/README.md index 9b068e1d..a9738d35 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,13 @@ ticloud cluster create Please check the CLI help for more information. -Documentation page is on the way. +### Set up TiDB Cloud API host + +Usually you don't need to set up the TiDB Cloud API url, the default value is `https://api.tidbcloud.com`. + +```shell +ticloud config set api-url https://api.tidbcloud.com +``` ## Roadmap diff --git a/internal/cli/cluster/create.go b/internal/cli/cluster/create.go index 9556b7fc..ee211591 100644 --- a/internal/cli/cluster/create.go +++ b/internal/cli/cluster/create.go @@ -98,7 +98,10 @@ func CreateCmd(h *internal.Helper) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - d := h.Client() + d, err := h.Client() + if err != nil { + return err + } var clusterName string var clusterType string @@ -179,7 +182,7 @@ func CreateCmd(h *internal.Helper) *cobra.Command { } region = regionModel.(ui.SelectModel).Choices[regionModel.(ui.SelectModel).Selected].(string) - project, err := cloud.GetSelectedProject(h.QueryPageSize, h.Client()) + project, err := cloud.GetSelectedProject(h.QueryPageSize, d) if err != nil { return err } @@ -241,7 +244,7 @@ func CreateCmd(h *internal.Helper) *cobra.Command { clusterDefBody := &clusterApi.CreateClusterBody{} - err := clusterDefBody.UnmarshalBinary([]byte(fmt.Sprintf(`{ + err = clusterDefBody.UnmarshalBinary([]byte(fmt.Sprintf(`{ "name": "%s", "cluster_type": "%s", "cloud_provider": "%s", diff --git a/internal/cli/cluster/create_test.go b/internal/cli/cluster/create_test.go index 968372ed..f4bd62d9 100644 --- a/internal/cli/cluster/create_test.go +++ b/internal/cli/cluster/create_test.go @@ -45,8 +45,8 @@ func (suite *CreateClusterSuite) SetupTest() { var pageSize int64 = 10 suite.mockClient = new(mock.ApiClient) suite.h = &internal.Helper{ - Client: func() cloud.TiDBCloudClient { - return suite.mockClient + Client: func() (cloud.TiDBCloudClient, error) { + return suite.mockClient, nil }, QueryPageSize: pageSize, IOStreams: iostream.Test(), diff --git a/internal/cli/cluster/delete.go b/internal/cli/cluster/delete.go index 672f1dc9..57fb4363 100644 --- a/internal/cli/cluster/delete.go +++ b/internal/cli/cluster/delete.go @@ -82,7 +82,10 @@ func DeleteCmd(h *internal.Helper) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - d := h.Client() + d, err := h.Client() + if err != nil { + return err + } var projectID string var clusterID string @@ -92,13 +95,13 @@ func DeleteCmd(h *internal.Helper) *cobra.Command { } // interactive mode - project, err := cloud.GetSelectedProject(h.QueryPageSize, h.Client()) + project, err := cloud.GetSelectedProject(h.QueryPageSize, d) if err != nil { return err } projectID = project.ID - cluster, err := cloud.GetSelectedCluster(projectID, h.QueryPageSize, h.Client()) + cluster, err := cloud.GetSelectedCluster(projectID, h.QueryPageSize, d) if err != nil { return err } @@ -147,7 +150,7 @@ func DeleteCmd(h *internal.Helper) *cobra.Command { params := clusterApi.NewDeleteClusterParams(). WithProjectID(projectID). WithClusterID(clusterID) - _, err := d.DeleteCluster(params) + _, err = d.DeleteCluster(params) if err != nil { return errors.Trace(err) } diff --git a/internal/cli/cluster/delete_test.go b/internal/cli/cluster/delete_test.go index 95140471..aee2731a 100644 --- a/internal/cli/cluster/delete_test.go +++ b/internal/cli/cluster/delete_test.go @@ -44,8 +44,8 @@ func (suite *DeleteClusterSuite) SetupTest() { var pageSize int64 = 10 suite.mockClient = new(mock.ApiClient) suite.h = &internal.Helper{ - Client: func() cloud.TiDBCloudClient { - return suite.mockClient + Client: func() (cloud.TiDBCloudClient, error) { + return suite.mockClient, nil }, QueryPageSize: pageSize, IOStreams: iostream.Test(), diff --git a/internal/cli/cluster/describe.go b/internal/cli/cluster/describe.go index 39485837..c80d2f5f 100644 --- a/internal/cli/cluster/describe.go +++ b/internal/cli/cluster/describe.go @@ -75,7 +75,10 @@ func DescribeCmd(h *internal.Helper) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - d := h.Client() + d, err := h.Client() + if err != nil { + return err + } var projectID string var clusterID string @@ -85,13 +88,13 @@ func DescribeCmd(h *internal.Helper) *cobra.Command { } // interactive mode - project, err := cloud.GetSelectedProject(h.QueryPageSize, h.Client()) + project, err := cloud.GetSelectedProject(h.QueryPageSize, d) if err != nil { return err } projectID = project.ID - cluster, err := cloud.GetSelectedCluster(projectID, h.QueryPageSize, h.Client()) + cluster, err := cloud.GetSelectedCluster(projectID, h.QueryPageSize, d) if err != nil { return err } diff --git a/internal/cli/cluster/describe_test.go b/internal/cli/cluster/describe_test.go index 88f4e38a..7b190bd1 100644 --- a/internal/cli/cluster/describe_test.go +++ b/internal/cli/cluster/describe_test.go @@ -91,8 +91,8 @@ func (suite *DescribeClusterSuite) SetupTest() { var pageSize int64 = 10 suite.mockClient = new(mock.ApiClient) suite.h = &internal.Helper{ - Client: func() cloud.TiDBCloudClient { - return suite.mockClient + Client: func() (cloud.TiDBCloudClient, error) { + return suite.mockClient, nil }, QueryPageSize: pageSize, IOStreams: iostream.Test(), diff --git a/internal/cli/cluster/list.go b/internal/cli/cluster/list.go index 8597b66e..f824ab12 100644 --- a/internal/cli/cluster/list.go +++ b/internal/cli/cluster/list.go @@ -55,6 +55,11 @@ func ListCmd(h *internal.Helper) *cobra.Command { } }, RunE: func(cmd *cobra.Command, args []string) error { + d, err := h.Client() + if err != nil { + return err + } + var pID string if opts.interactive { if !h.IOStreams.CanPrompt { @@ -62,7 +67,7 @@ func ListCmd(h *internal.Helper) *cobra.Command { } // interactive mode - project, err := cloud.GetSelectedProject(h.QueryPageSize, h.Client()) + project, err := cloud.GetSelectedProject(h.QueryPageSize, d) if err != nil { return err } @@ -71,7 +76,7 @@ func ListCmd(h *internal.Helper) *cobra.Command { pID = args[0] } - total, items, err := cloud.RetrieveClusters(pID, h.QueryPageSize, h.Client()) + total, items, err := cloud.RetrieveClusters(pID, h.QueryPageSize, d) if err != nil { return err } diff --git a/internal/cli/cluster/list_test.go b/internal/cli/cluster/list_test.go index ed60db2c..4d86ada3 100644 --- a/internal/cli/cluster/list_test.go +++ b/internal/cli/cluster/list_test.go @@ -191,8 +191,8 @@ func (suite *ListClusterSuite) SetupTest() { var pageSize int64 = 10 suite.mockClient = new(mock.ApiClient) suite.h = &internal.Helper{ - Client: func() cloud.TiDBCloudClient { - return suite.mockClient + Client: func() (cloud.TiDBCloudClient, error) { + return suite.mockClient, nil }, QueryPageSize: pageSize, IOStreams: iostream.Test(), diff --git a/internal/cli/config/set.go b/internal/cli/config/set.go index 0de9199c..4dba69d7 100644 --- a/internal/cli/config/set.go +++ b/internal/cli/config/set.go @@ -53,6 +53,13 @@ If not, the config in the active profile will be set`, prop.ProfileProperties()) if curP == "" { return fmt.Errorf("no profile is configured, please use `config create` to create a profile") } + + if propertyName == prop.ApiUrl { + _, err := prop.ValidateApiUrl(value) + if err != nil { + return err + } + } viper.Set(fmt.Sprintf("%s.%s", curP, propertyName), value) res = fmt.Sprintf("Set profile `%s` property `%s` to value `%s` successfully", curP, propertyName, value) } else { diff --git a/internal/cli/config/set_test.go b/internal/cli/config/set_test.go index f9f2a77c..bad2992b 100644 --- a/internal/cli/config/set_test.go +++ b/internal/cli/config/set_test.go @@ -17,6 +17,7 @@ package config import ( "bytes" "fmt" + "net/url" "os" "testing" @@ -25,6 +26,7 @@ import ( "tidbcloud-cli/internal/iostream" "tidbcloud-cli/internal/util" + "github.com/juju/errors" "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -96,6 +98,15 @@ func (suite *SetConfigSuite) TestSetConfigArgs() { args: []string{"unknown", "value"}, err: fmt.Errorf("unrecognized property `unknown`, use `config set --help` to find available properties"), }, + { + name: "set config with unknown property", + args: []string{"api-url", "baidu.com"}, + err: errors.Annotate(&url.Error{ + Op: "parse", + URL: "baidu.com", + Err: fmt.Errorf("invalid URI for request"), + }, "api url should format as ://"), + }, } for _, tt := range tests { @@ -108,7 +119,11 @@ func (suite *SetConfigSuite) TestSetConfigArgs() { suite.h.IOStreams.Err.(*bytes.Buffer).Reset() cmd.SetArgs(tt.args) err = cmd.Execute() - assert.Equal(tt.err, err) + if err != nil { + assert.EqualError(tt.err, err.Error()) + } else { + assert.Equal(tt.err, err) + } assert.Equal(tt.stdoutString, suite.h.IOStreams.Out.(*bytes.Buffer).String()) assert.Equal(tt.stderrString, suite.h.IOStreams.Err.(*bytes.Buffer).String()) diff --git a/internal/cli/project/list.go b/internal/cli/project/list.go index 323deed9..f6f646eb 100644 --- a/internal/cli/project/list.go +++ b/internal/cli/project/list.go @@ -40,7 +40,11 @@ func ListCmd(h *internal.Helper) *cobra.Command { List the projects with json format: $ %[1]s project list -o json`, config.CliName), RunE: func(cmd *cobra.Command, args []string) error { - total, items, err := cloud.RetrieveProjects(h.QueryPageSize, h.Client()) + d, err := h.Client() + if err != nil { + return err + } + total, items, err := cloud.RetrieveProjects(h.QueryPageSize, d) if err != nil { return err } diff --git a/internal/cli/project/list_test.go b/internal/cli/project/list_test.go index fb279794..f669c677 100644 --- a/internal/cli/project/list_test.go +++ b/internal/cli/project/list_test.go @@ -83,8 +83,8 @@ func (suite *ListProjectSuite) SetupTest() { var pageSize int64 = 10 suite.mockClient = new(mock.ApiClient) suite.h = &internal.Helper{ - Client: func() cloud.TiDBCloudClient { - return suite.mockClient + Client: func() (cloud.TiDBCloudClient, error) { + return suite.mockClient, nil }, QueryPageSize: pageSize, IOStreams: iostream.Test(), diff --git a/internal/cli/root.go b/internal/cli/root.go index b352b40b..64442720 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -46,9 +46,18 @@ func Execute(ctx context.Context, ver, commit, buildDate string) { } h := &internal.Helper{ - Client: func() cloud.TiDBCloudClient { + Client: func() (cloud.TiDBCloudClient, error) { publicKey, privateKey := util.GetAccessKeys(c.ActiveProfile) - return cloud.NewClientDelegate(publicKey, privateKey) + apiUrl := util.GetApiUrl(c.ActiveProfile) + // If the user has not set the api url, use the default one. + if apiUrl == "" { + apiUrl = cloud.DefaultApiUrl + } + delegate, err := cloud.NewClientDelegate(publicKey, privateKey, apiUrl) + if err != nil { + return nil, err + } + return delegate, nil }, QueryPageSize: internal.DefaultPageSize, IOStreams: iostream.System(), diff --git a/internal/helper.go b/internal/helper.go index eef823d6..fdf7256d 100644 --- a/internal/helper.go +++ b/internal/helper.go @@ -25,7 +25,7 @@ const ( ) type Helper struct { - Client func() cloud.TiDBCloudClient + Client func() (cloud.TiDBCloudClient, error) QueryPageSize int64 IOStreams *iostream.IOStreams Config *config.Config diff --git a/internal/prop/property.go b/internal/prop/property.go index fe98cee8..11b34cc9 100644 --- a/internal/prop/property.go +++ b/internal/prop/property.go @@ -14,10 +14,17 @@ package prop +import ( + "net/url" + + "github.com/juju/errors" +) + const ( PublicKey string = "public-key" PrivateKey string = "private-key" CurProfile string = "current-profile" + ApiUrl string = "api-url" ) func GlobalProperties() []string { @@ -25,9 +32,17 @@ func GlobalProperties() []string { } func ProfileProperties() []string { - return []string{PublicKey, PrivateKey} + return []string{PublicKey, PrivateKey, ApiUrl} } func Properties() []string { - return []string{PublicKey, PrivateKey, CurProfile} + return []string{PublicKey, PrivateKey, CurProfile, ApiUrl} +} + +func ValidateApiUrl(value string) (*url.URL, error) { + u, err := url.ParseRequestURI(value) + if err != nil { + return nil, errors.Annotate(err, "api url should format as ://") + } + return u, nil } diff --git a/internal/service/cloud/api_client.go b/internal/service/cloud/api_client.go index 3ac4d3a3..868259ab 100644 --- a/internal/service/cloud/api_client.go +++ b/internal/service/cloud/api_client.go @@ -17,6 +17,8 @@ package cloud import ( "net/http" + "tidbcloud-cli/internal/prop" + apiClient "github.com/c4pt0r/go-tidbcloud-sdk-v1/client" "github.com/c4pt0r/go-tidbcloud-sdk-v1/client/cluster" "github.com/c4pt0r/go-tidbcloud-sdk-v1/client/project" @@ -26,7 +28,7 @@ import ( ) const ( - apiBaseUrl = "api.tidbcloud.com" + DefaultApiUrl = "https://api.tidbcloud.com" ) type TiDBCloudClient interface { @@ -47,10 +49,14 @@ type ClientDelegate struct { c *apiClient.GoTidbcloud } -func NewClientDelegate(publicKey string, privateKey string) *ClientDelegate { - return &ClientDelegate{ - c: NewApiClient(publicKey, privateKey), +func NewClientDelegate(publicKey string, privateKey string, apiUrl string) (*ClientDelegate, error) { + client, err := NewApiClient(publicKey, privateKey, apiUrl) + if err != nil { + return nil, err } + return &ClientDelegate{ + c: client, + }, nil } func (d *ClientDelegate) CreateCluster(params *cluster.CreateClusterParams, opts ...cluster.ClientOption) (*cluster.CreateClusterOK, error) { @@ -77,12 +83,19 @@ func (d *ClientDelegate) ListProjects(params *project.ListProjectsParams, opts . return d.c.Project.ListProjects(params, opts...) } -func NewApiClient(publicKey string, privateKey string) *apiClient.GoTidbcloud { +func NewApiClient(publicKey string, privateKey string, apiUrl string) (*apiClient.GoTidbcloud, error) { httpclient := &http.Client{ Transport: &digest.Transport{ Username: publicKey, Password: privateKey, }, } - return apiClient.New(httpTransport.NewWithClient(apiBaseUrl, "/", []string{"https"}, httpclient), strfmt.Default) + + // Parse the URL + u, err := prop.ValidateApiUrl(apiUrl) + if err != nil { + return nil, err + } + + return apiClient.New(httpTransport.NewWithClient(u.Host, u.Path, []string{u.Scheme}, httpclient), strfmt.Default), nil } diff --git a/internal/util/auth.go b/internal/util/auth.go index bba8591c..dcbabb53 100644 --- a/internal/util/auth.go +++ b/internal/util/auth.go @@ -45,3 +45,8 @@ func GetAccessKeys(profile string) (publicKey string, privateKey string) { privateKey = viper.GetString(fmt.Sprintf("%s.%s", profile, prop.PrivateKey)) return } + +func GetApiUrl(profile string) (apiUrl string) { + apiUrl = viper.GetString(fmt.Sprintf("%s.%s", profile, prop.ApiUrl)) + return +}