From cfe4b9a72a98e655527f4f08b4b9e1a14b81091c Mon Sep 17 00:00:00 2001 From: sjcsjc123 <1401189096@qq.com> Date: Fri, 16 Jun 2023 00:33:24 +0800 Subject: [PATCH 01/10] fix bug with upload etcd file fail in the case of `OS:linux` and `Arch:amd64` --- pkg/deployer/baremetal/artifacts.go | 62 ++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/pkg/deployer/baremetal/artifacts.go b/pkg/deployer/baremetal/artifacts.go index 6ed74852..c383117d 100644 --- a/pkg/deployer/baremetal/artifacts.go +++ b/pkg/deployer/baremetal/artifacts.go @@ -27,6 +27,7 @@ import ( "path" "path/filepath" "runtime" + "strings" "github.com/GreptimeTeam/gtctl/pkg/logger" "github.com/GreptimeTeam/gtctl/pkg/utils" @@ -38,6 +39,9 @@ const ( EtcdGitHubOrg = "etcd-io" EtcdGithubRepo = "etcd" + + ZipExtension = ".zip" + TarGzExtension = ".tar.gz" ) // ArtifactManager is responsible for managing the artifacts of a GreptimeDB cluster. @@ -147,6 +151,9 @@ func (am *ArtifactManager) installEtcd(artifactFile, pkgDir, binDir string) erro artifactFile = path.Base(artifactFile) // If the artifactFile is '${pkgDir}/etcd-v3.5.7-darwin-arm64.zip', it will get '${pkgDir}/etcd-v3.5.7-darwin-arm64'. uncompressedDir := path.Join(pkgDir, artifactFile[:len(artifactFile)-len(filepath.Ext(artifactFile))]) + if strings.HasSuffix(uncompressedDir, ".tar") { + uncompressedDir = uncompressedDir[:len(uncompressedDir)-len(".tar")] + } binaries := []string{"etcd", "etcdctl", "etcdutl"} for _, binary := range binaries { if err := am.copyFile(path.Join(uncompressedDir, binary), path.Join(binDir, binary)); err != nil { @@ -176,7 +183,7 @@ func (am *ArtifactManager) installGreptime(artifactFile, binDir string) error { } func (am *ArtifactManager) download(typ ArtifactType, version, pkgDir string) (string, error) { - downloadURL, err := am.artifactURL(typ, version) + downloadURL, err := am.artifactURL(typ, version, ZipExtension) if err != nil { return "", err } @@ -200,16 +207,33 @@ func (am *ArtifactManager) download(typ ArtifactType, version, pkgDir string) (s } httpClient := &http.Client{} - req, err := http.NewRequest(http.MethodGet, downloadURL, nil) - if err != nil { - return "", err - } am.logger.V(3).Infof("Downloading artifact from '%s' to '%s'", downloadURL, artifactFile) - resp, err := httpClient.Do(req) + resp, err := am.startDownload(downloadURL, httpClient) if err != nil { - return "", err + downloadURL, err = am.artifactURL(typ, version, TarGzExtension) + if err != nil { + return "", err + } + artifactFile = path.Join(pkgDir, path.Base(downloadURL)) + if !am.alwaysDownload { + // The artifact file already exists, skip downloading. + if _, err := os.Stat(artifactFile); err == nil { + am.logger.V(3).Infof("The artifact file '%s' already exists, skip downloading.", artifactFile) + return artifactFile, nil + } + + // Other error happened, return it. + if err != nil && !os.IsNotExist(err) { + return "", err + } + } + resp, err = am.startDownload(downloadURL, httpClient) + if err != nil { + return "", err + } + } defer resp.Body.Close() @@ -231,7 +255,23 @@ func (am *ArtifactManager) download(typ ArtifactType, version, pkgDir string) (s return artifactFile, nil } -func (am *ArtifactManager) artifactURL(typ ArtifactType, version string) (string, error) { +func (am *ArtifactManager) startDownload(downloadURL string, client *http.Client) (*http.Response, error) { + resp := &http.Response{} + request, err := http.NewRequest(http.MethodGet, downloadURL, nil) + if err != nil { + return resp, err + } + resp, err = client.Do(request) + if resp.StatusCode != http.StatusOK { + return resp, fmt.Errorf("download failed, status code: %d", resp.StatusCode) + } + if err != nil { + return resp, err + } + return resp, nil +} + +func (am *ArtifactManager) artifactURL(typ ArtifactType, version, ext string) (string, error) { switch typ { case GreptimeArtifactType: var downloadURL string @@ -245,8 +285,8 @@ func (am *ArtifactManager) artifactURL(typ ArtifactType, version string) (string return downloadURL, nil case EtcdArtifactType: // For the function stability, we use the specific version of etcd. - downloadURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s-%s-%s-%s.zip", - EtcdGitHubOrg, EtcdGithubRepo, version, string(typ), version, runtime.GOOS, runtime.GOARCH) + downloadURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s-%s-%s-%s%s", + EtcdGitHubOrg, EtcdGithubRepo, version, string(typ), version, runtime.GOOS, runtime.GOARCH, ext) return downloadURL, nil default: return "", fmt.Errorf("unsupported artifact type: %v", typ) @@ -260,6 +300,8 @@ func (am *ArtifactManager) uncompress(file, dst string) error { return am.unzip(file, dst) case ".tgz": return am.untar(file, dst) + case ".gz": + return am.untar(file, dst) default: return fmt.Errorf("unsupported file type: %s", fileType) } From 5a418a360388f7f5f1feb5eee9a5bd4f1c1470d0 Mon Sep 17 00:00:00 2001 From: sjcsjc123 <1401189096@qq.com> Date: Fri, 16 Jun 2023 00:37:09 +0800 Subject: [PATCH 02/10] fix bug with upload etcd file fail in the case of `OS:linux` and `Arch:amd64` --- pkg/deployer/baremetal/artifacts.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/deployer/baremetal/artifacts.go b/pkg/deployer/baremetal/artifacts.go index c383117d..10f22daa 100644 --- a/pkg/deployer/baremetal/artifacts.go +++ b/pkg/deployer/baremetal/artifacts.go @@ -42,6 +42,7 @@ const ( ZipExtension = ".zip" TarGzExtension = ".tar.gz" + TarExtension = ".tar" ) // ArtifactManager is responsible for managing the artifacts of a GreptimeDB cluster. @@ -151,8 +152,8 @@ func (am *ArtifactManager) installEtcd(artifactFile, pkgDir, binDir string) erro artifactFile = path.Base(artifactFile) // If the artifactFile is '${pkgDir}/etcd-v3.5.7-darwin-arm64.zip', it will get '${pkgDir}/etcd-v3.5.7-darwin-arm64'. uncompressedDir := path.Join(pkgDir, artifactFile[:len(artifactFile)-len(filepath.Ext(artifactFile))]) - if strings.HasSuffix(uncompressedDir, ".tar") { - uncompressedDir = uncompressedDir[:len(uncompressedDir)-len(".tar")] + if strings.HasSuffix(uncompressedDir, TarExtension) { + uncompressedDir = uncompressedDir[:len(uncompressedDir)-len(TarExtension)] } binaries := []string{"etcd", "etcdctl", "etcdutl"} for _, binary := range binaries { From 5f0dce9c7c4b67825dd218391c65be29e7e4fff7 Mon Sep 17 00:00:00 2001 From: sjcsjc123 <1401189096@qq.com> Date: Sat, 17 Jun 2023 21:07:36 +0800 Subject: [PATCH 03/10] run `golanglint` has make error: `TrimSuffix doesn't have side effects and its return value is ignored` --- pkg/deployer/baremetal/artifacts.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/deployer/baremetal/artifacts.go b/pkg/deployer/baremetal/artifacts.go index 10f22daa..569a4a79 100644 --- a/pkg/deployer/baremetal/artifacts.go +++ b/pkg/deployer/baremetal/artifacts.go @@ -152,9 +152,7 @@ func (am *ArtifactManager) installEtcd(artifactFile, pkgDir, binDir string) erro artifactFile = path.Base(artifactFile) // If the artifactFile is '${pkgDir}/etcd-v3.5.7-darwin-arm64.zip', it will get '${pkgDir}/etcd-v3.5.7-darwin-arm64'. uncompressedDir := path.Join(pkgDir, artifactFile[:len(artifactFile)-len(filepath.Ext(artifactFile))]) - if strings.HasSuffix(uncompressedDir, TarExtension) { - uncompressedDir = uncompressedDir[:len(uncompressedDir)-len(TarExtension)] - } + uncompressedDir = strings.TrimSuffix(uncompressedDir, TarExtension) binaries := []string{"etcd", "etcdctl", "etcdutl"} for _, binary := range binaries { if err := am.copyFile(path.Join(uncompressedDir, binary), path.Join(binDir, binary)); err != nil { From b50c5e1d6cf1d7cdd039456d4eedaacbce7095c6 Mon Sep 17 00:00:00 2001 From: sjcsjc123 <1401189096@qq.com> Date: Mon, 19 Jun 2023 15:45:15 +0800 Subject: [PATCH 04/10] determine ext by `GOOS` --- pkg/deployer/baremetal/artifacts.go | 61 ++++++++++------------------- 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/pkg/deployer/baremetal/artifacts.go b/pkg/deployer/baremetal/artifacts.go index 569a4a79..852d0e44 100644 --- a/pkg/deployer/baremetal/artifacts.go +++ b/pkg/deployer/baremetal/artifacts.go @@ -43,6 +43,9 @@ const ( ZipExtension = ".zip" TarGzExtension = ".tar.gz" TarExtension = ".tar" + + GOOSDarwin = "darwin" + GOOSLinux = "linux" ) // ArtifactManager is responsible for managing the artifacts of a GreptimeDB cluster. @@ -182,7 +185,15 @@ func (am *ArtifactManager) installGreptime(artifactFile, binDir string) error { } func (am *ArtifactManager) download(typ ArtifactType, version, pkgDir string) (string, error) { - downloadURL, err := am.artifactURL(typ, version, ZipExtension) + var extension string + if runtime.GOOS == GOOSDarwin { + extension = ZipExtension + } else if runtime.GOOS == GOOSLinux { + extension = TarGzExtension + } else { + return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS) + } + downloadURL, err := am.artifactURL(typ, version, extension) if err != nil { return "", err } @@ -209,30 +220,16 @@ func (am *ArtifactManager) download(typ ArtifactType, version, pkgDir string) (s am.logger.V(3).Infof("Downloading artifact from '%s' to '%s'", downloadURL, artifactFile) - resp, err := am.startDownload(downloadURL, httpClient) + req, err := http.NewRequest(http.MethodGet, downloadURL, nil) if err != nil { - downloadURL, err = am.artifactURL(typ, version, TarGzExtension) - if err != nil { - return "", err - } - artifactFile = path.Join(pkgDir, path.Base(downloadURL)) - if !am.alwaysDownload { - // The artifact file already exists, skip downloading. - if _, err := os.Stat(artifactFile); err == nil { - am.logger.V(3).Infof("The artifact file '%s' already exists, skip downloading.", artifactFile) - return artifactFile, nil - } - - // Other error happened, return it. - if err != nil && !os.IsNotExist(err) { - return "", err - } - } - resp, err = am.startDownload(downloadURL, httpClient) - if err != nil { - return "", err - } - + return "", err + } + resp, err := httpClient.Do(req) + if err != nil { + return "", err + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("download failed, status code: %d", resp.StatusCode) } defer resp.Body.Close() @@ -254,22 +251,6 @@ func (am *ArtifactManager) download(typ ArtifactType, version, pkgDir string) (s return artifactFile, nil } -func (am *ArtifactManager) startDownload(downloadURL string, client *http.Client) (*http.Response, error) { - resp := &http.Response{} - request, err := http.NewRequest(http.MethodGet, downloadURL, nil) - if err != nil { - return resp, err - } - resp, err = client.Do(request) - if resp.StatusCode != http.StatusOK { - return resp, fmt.Errorf("download failed, status code: %d", resp.StatusCode) - } - if err != nil { - return resp, err - } - return resp, nil -} - func (am *ArtifactManager) artifactURL(typ ArtifactType, version, ext string) (string, error) { switch typ { case GreptimeArtifactType: From d9994289e925c655f4ba8368957ebce11b2f1b04 Mon Sep 17 00:00:00 2001 From: sjcsjc123 <1401189096@qq.com> Date: Wed, 28 Jun 2023 13:07:44 +0800 Subject: [PATCH 05/10] modify const and code style --- pkg/deployer/baremetal/artifacts.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/deployer/baremetal/artifacts.go b/pkg/deployer/baremetal/artifacts.go index 852d0e44..5ae1735a 100644 --- a/pkg/deployer/baremetal/artifacts.go +++ b/pkg/deployer/baremetal/artifacts.go @@ -43,6 +43,8 @@ const ( ZipExtension = ".zip" TarGzExtension = ".tar.gz" TarExtension = ".tar" + TgzExtension = ".tgz" + GzExtension = ".gz" GOOSDarwin = "darwin" GOOSLinux = "linux" @@ -186,11 +188,12 @@ func (am *ArtifactManager) installGreptime(artifactFile, binDir string) error { func (am *ArtifactManager) download(typ ArtifactType, version, pkgDir string) (string, error) { var extension string - if runtime.GOOS == GOOSDarwin { + switch runtime.GOOS { + case GOOSDarwin: extension = ZipExtension - } else if runtime.GOOS == GOOSLinux { + case GOOSLinux: extension = TarGzExtension - } else { + default: return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS) } downloadURL, err := am.artifactURL(typ, version, extension) @@ -276,11 +279,11 @@ func (am *ArtifactManager) artifactURL(typ ArtifactType, version, ext string) (s func (am *ArtifactManager) uncompress(file, dst string) error { fileType := path.Ext(file) switch fileType { - case ".zip": + case ZipExtension: return am.unzip(file, dst) - case ".tgz": + case TgzExtension: return am.untar(file, dst) - case ".gz": + case GzExtension: return am.untar(file, dst) default: return fmt.Errorf("unsupported file type: %s", fileType) From 1dfc06fd8f6cc77e7fe2f2dfa49455754c5d9e7b Mon Sep 17 00:00:00 2001 From: sjcsjc123 <1401189096@qq.com> Date: Wed, 28 Jun 2023 15:31:58 +0800 Subject: [PATCH 06/10] add `gtctl connect mysql://user:password@host:port` command --- pkg/cmd/gtctl/connect/connect.go | 48 ++++++++++++++++++++++ pkg/cmd/gtctl/connect/mysql/mysql.go | 59 ++++++++++++++++++++++++++++ pkg/cmd/gtctl/root.go | 2 + 3 files changed, 109 insertions(+) create mode 100644 pkg/cmd/gtctl/connect/connect.go create mode 100644 pkg/cmd/gtctl/connect/mysql/mysql.go diff --git a/pkg/cmd/gtctl/connect/connect.go b/pkg/cmd/gtctl/connect/connect.go new file mode 100644 index 00000000..4018952b --- /dev/null +++ b/pkg/cmd/gtctl/connect/connect.go @@ -0,0 +1,48 @@ +// Copyright 2023 Greptime Team +// +// 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 connect + +import ( + "errors" + "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/connect/mysql" + "github.com/GreptimeTeam/gtctl/pkg/logger" + "github.com/spf13/cobra" + "strings" +) + +func NewConnectCommand(l logger.Logger) *cobra.Command { + cmd := &cobra.Command{ + Use: "connect", + Short: "Connect to a GreptimeDB cluster", + Long: `Connect to a GreptimeDB cluster`, + RunE: connectCommand, + } + return cmd +} + +func connectCommand(cmd *cobra.Command, args []string) error { + s := args[0] + split := strings.Split(s, "://") + if len(split) != 2 { + return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") + } + prefix := split[0] + switch prefix { + case "mysql": + return mysql.ConnectCommand(cmd, args) + default: + return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") + } +} diff --git a/pkg/cmd/gtctl/connect/mysql/mysql.go b/pkg/cmd/gtctl/connect/mysql/mysql.go new file mode 100644 index 00000000..fc20ba3a --- /dev/null +++ b/pkg/cmd/gtctl/connect/mysql/mysql.go @@ -0,0 +1,59 @@ +// Copyright 2023 Greptime Team +// +// 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 mysql + +import ( + "errors" + "github.com/spf13/cobra" + "log" + "os" + "os/exec" + "strings" +) + +func ConnectCommand(cmd *cobra.Command, args []string) error { + s := args[0] + suffix := strings.Split(s, "://")[1] + split := strings.Split(suffix, "@") + if len(split) != 2 { + return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") + } + up := strings.Split(split[0], ":") + if len(up) != 2 { + return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") + } + hp := strings.Split(split[1], ":") + if len(hp) != 2 { + return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") + } + user := up[0] + password := up[1] + host := hp[0] + port := hp[1] + return Connect(user, password, host, port) +} + +func Connect(user, password, host, port string) error { + cmd := exec.Command("mysql", "-h", host, "-P", port, "-u", user, "-p", password) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + err = cmd.Wait() + return err +} diff --git a/pkg/cmd/gtctl/root.go b/pkg/cmd/gtctl/root.go index 9df8cf2d..6ab76b21 100644 --- a/pkg/cmd/gtctl/root.go +++ b/pkg/cmd/gtctl/root.go @@ -16,6 +16,7 @@ package gtctl import ( "fmt" + "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/connect" "os" "github.com/spf13/cobra" @@ -62,6 +63,7 @@ func NewRootCommand() *cobra.Command { // Add all top level subcommands. cmd.AddCommand(version.NewVersionCommand(l)) cmd.AddCommand(cluster.NewClusterCommand(l)) + cmd.AddCommand(connect.NewConnectCommand(l)) return cmd } From 155a9cb9bdd2e32f3b8362691443230396959ef2 Mon Sep 17 00:00:00 2001 From: sjcsjc123 <1401189096@qq.com> Date: Wed, 28 Jun 2023 15:42:10 +0800 Subject: [PATCH 07/10] add `gtctl connect mysql://user:password@host:port` command --- pkg/cmd/gtctl/connect/connect.go | 9 +++++++-- pkg/cmd/gtctl/connect/mysql/mysql.go | 21 ++++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/gtctl/connect/connect.go b/pkg/cmd/gtctl/connect/connect.go index 4018952b..13a900bd 100644 --- a/pkg/cmd/gtctl/connect/connect.go +++ b/pkg/cmd/gtctl/connect/connect.go @@ -22,6 +22,11 @@ import ( "strings" ) +const ( + SplitSeparator = "://" + MySQL = "mysql" +) + func NewConnectCommand(l logger.Logger) *cobra.Command { cmd := &cobra.Command{ Use: "connect", @@ -34,13 +39,13 @@ func NewConnectCommand(l logger.Logger) *cobra.Command { func connectCommand(cmd *cobra.Command, args []string) error { s := args[0] - split := strings.Split(s, "://") + split := strings.Split(s, SplitSeparator) if len(split) != 2 { return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") } prefix := split[0] switch prefix { - case "mysql": + case MySQL: return mysql.ConnectCommand(cmd, args) default: return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") diff --git a/pkg/cmd/gtctl/connect/mysql/mysql.go b/pkg/cmd/gtctl/connect/mysql/mysql.go index fc20ba3a..3b588658 100644 --- a/pkg/cmd/gtctl/connect/mysql/mysql.go +++ b/pkg/cmd/gtctl/connect/mysql/mysql.go @@ -23,18 +23,29 @@ import ( "strings" ) +const ( + UpSeparator = ":" + AtSeparator = "@" + ArgHost = "-h" + ArgPort = "-P" + ArgUser = "-u" + ArgPassword = "-p" + MySQL = "mysql" + SplitSeparator = "://" +) + func ConnectCommand(cmd *cobra.Command, args []string) error { s := args[0] - suffix := strings.Split(s, "://")[1] - split := strings.Split(suffix, "@") + suffix := strings.Split(s, SplitSeparator)[1] + split := strings.Split(suffix, AtSeparator) if len(split) != 2 { return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") } - up := strings.Split(split[0], ":") + up := strings.Split(split[0], UpSeparator) if len(up) != 2 { return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") } - hp := strings.Split(split[1], ":") + hp := strings.Split(split[1], UpSeparator) if len(hp) != 2 { return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") } @@ -46,7 +57,7 @@ func ConnectCommand(cmd *cobra.Command, args []string) error { } func Connect(user, password, host, port string) error { - cmd := exec.Command("mysql", "-h", host, "-P", port, "-u", user, "-p", password) + cmd := exec.Command(MySQL, ArgHost, host, ArgPort, port, ArgUser, user, ArgPassword, password) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin From 3c3468634e51f22c2a151e21050a31cb0451aecf Mon Sep 17 00:00:00 2001 From: sjcsjc123 <1401189096@qq.com> Date: Mon, 24 Jul 2023 20:24:05 +0800 Subject: [PATCH 08/10] Implement gtctl cluster connect mysql subcommand #3 --- pkg/cmd/gtctl/cluster/cluster.go | 2 + pkg/cmd/gtctl/cluster/connect/connect.go | 93 ++++++++++++++++ pkg/cmd/gtctl/cluster/connect/mysql/mysql.go | 108 +++++++++++++++++++ pkg/cmd/gtctl/cluster/connect/pg/pg.go | 24 +++++ pkg/cmd/gtctl/connect/connect.go | 53 --------- pkg/cmd/gtctl/connect/mysql/mysql.go | 70 ------------ pkg/cmd/gtctl/root.go | 2 - 7 files changed, 227 insertions(+), 125 deletions(-) create mode 100644 pkg/cmd/gtctl/cluster/connect/connect.go create mode 100644 pkg/cmd/gtctl/cluster/connect/mysql/mysql.go create mode 100644 pkg/cmd/gtctl/cluster/connect/pg/pg.go delete mode 100644 pkg/cmd/gtctl/connect/connect.go delete mode 100644 pkg/cmd/gtctl/connect/mysql/mysql.go diff --git a/pkg/cmd/gtctl/cluster/cluster.go b/pkg/cmd/gtctl/cluster/cluster.go index 4e3c3c4d..e0961c6a 100644 --- a/pkg/cmd/gtctl/cluster/cluster.go +++ b/pkg/cmd/gtctl/cluster/cluster.go @@ -19,6 +19,7 @@ import ( "github.com/spf13/cobra" + "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/cluster/connect" "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/cluster/create" "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/cluster/delete" "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/cluster/get" @@ -47,6 +48,7 @@ func NewClusterCommand(l logger.Logger) *cobra.Command { cmd.AddCommand(scale.NewScaleClusterCommand(l)) cmd.AddCommand(get.NewGetClusterCommand(l)) cmd.AddCommand(list.NewListClustersCommand(l)) + cmd.AddCommand(connect.NewConnectCommand(l)) return cmd } diff --git a/pkg/cmd/gtctl/cluster/connect/connect.go b/pkg/cmd/gtctl/cluster/connect/connect.go new file mode 100644 index 00000000..49bc824f --- /dev/null +++ b/pkg/cmd/gtctl/cluster/connect/connect.go @@ -0,0 +1,93 @@ +// Copyright 2023 Greptime Team +// +// 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 connect + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + + greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" + "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/cluster/connect/mysql" + "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/cluster/connect/pg" + "github.com/GreptimeTeam/gtctl/pkg/deployer/k8s" + "github.com/GreptimeTeam/gtctl/pkg/logger" +) + +type getClusterCliOptions struct { + Namespace string +} + +func NewConnectCommand(l logger.Logger) *cobra.Command { + var options getClusterCliOptions + cmd := &cobra.Command{ + Use: "connect", + Short: "Connect to a GreptimeDB cluster", + Long: `Connect to a GreptimeDB cluster`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("cluster name should be set") + } + + k8sDeployer, err := k8s.NewDeployer(l) + if err != nil { + return err + } + + var ( + ctx = context.TODO() + clusterName = args[0] + namespace = options.Namespace + ) + + name := types.NamespacedName{ + Namespace: options.Namespace, + Name: clusterName, + }.String() + cluster, err := k8sDeployer.GetGreptimeDBCluster(ctx, name, nil) + if err != nil && errors.IsNotFound(err) { + l.Errorf("cluster %s in %s not found\n", clusterName, namespace) + return nil + } + rawCluster, ok := cluster.Raw.(*greptimedbclusterv1alpha1.GreptimeDBCluster) + if !ok { + return fmt.Errorf("invalid cluster type") + } + dbType := cmd.Flag("p") + switch strings.ToLower(dbType.Value.String()) { + case "mysql": + err := mysql.ConnectCommand(rawCluster, l) + if err != nil { + _ = fmt.Errorf("error connecting to mysql: %v", err) + } + case "pg": + err := pg.ConnectCommand(rawCluster, l) + if err != nil { + _ = fmt.Errorf("error connecting to postgres: %v", err) + } + default: + return fmt.Errorf("database type not supported") + } + return nil + }, + } + cmd.Flags().String("p", "mysql", "Specify a database") + cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "default", "Namespace of GreptimeDB cluster.") + return cmd +} diff --git a/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go b/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go new file mode 100644 index 00000000..ba4188bc --- /dev/null +++ b/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go @@ -0,0 +1,108 @@ +// Copyright 2023 Greptime Team +// +// 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 mysql + +import ( + "context" + "database/sql" + "os" + "os/exec" + "strconv" + "sync" + + "github.com/go-sql-driver/mysql" + + greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" + "github.com/GreptimeTeam/gtctl/pkg/logger" +) + +const ( + ArgHost = "-h" + ArgPort = "-P" + MySQL = "mysql" +) + +// ConnectCommand connects to a GreptimeDB cluster +func ConnectCommand(rawCluster *greptimedbclusterv1alpha1.GreptimeDBCluster, l logger.Logger) error { + return connect("127.0.0.1", strconv.Itoa(int(rawCluster.Spec.MySQLServicePort)), rawCluster.Name, l) +} + +// connect connects to a GreptimeDB cluster +func connect(host, port, clusterName string, l logger.Logger) error { + waitGroup := sync.WaitGroup{} + cmd := exec.CommandContext(context.Background(), "kubectl", "port-forward", "-n", "default", "svc/"+clusterName+"-frontend", port+":"+port) + err := cmd.Start() + if err != nil { + l.Errorf("Error starting port-forwarding: %v", err) + return err + } + go func() { + waitGroup.Add(1) + defer waitGroup.Done() + if err = cmd.Wait(); err != nil { + } + }() + for { + cfg := mysql.Config{ + Net: "tcp", + Addr: "127.0.0.1:4002", + User: "", + Passwd: "", + DBName: "", + AllowNativePasswords: true, + } + + db, err := sql.Open("mysql", cfg.FormatDSN()) + if err != nil { + continue + } + + _, err = db.Conn(context.Background()) + if err != nil { + continue + } + + err = db.Close() + if err != nil { + l.V(1).Infof("Error closing connection: %v", err) + return err + } + break + } + + cmd = exec.Command(MySQL, ArgHost, host, ArgPort, port) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + err = cmd.Start() + if err != nil { + l.Errorf("Error starting mysql client: %v", err) + return err + } + if err = cmd.Wait(); err != nil { + l.Errorf("Error waiting for mysql client to finish: %v", err) + return err + } + // gracefully stop port-forwarding + err = cmd.Process.Kill() + if err != nil { + if err.Error() != "os: process already finished" { + l.V(1).Info("Shutting down port-forwarding successfully") + } + return err + } + waitGroup.Wait() + return nil +} diff --git a/pkg/cmd/gtctl/cluster/connect/pg/pg.go b/pkg/cmd/gtctl/cluster/connect/pg/pg.go new file mode 100644 index 00000000..27293a0e --- /dev/null +++ b/pkg/cmd/gtctl/cluster/connect/pg/pg.go @@ -0,0 +1,24 @@ +// Copyright 2023 Greptime Team +// +// 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 pg + +import ( + greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" + "github.com/GreptimeTeam/gtctl/pkg/logger" +) + +func ConnectCommand(rawCluster *greptimedbclusterv1alpha1.GreptimeDBCluster, l logger.Logger) error { + return nil +} diff --git a/pkg/cmd/gtctl/connect/connect.go b/pkg/cmd/gtctl/connect/connect.go deleted file mode 100644 index 13a900bd..00000000 --- a/pkg/cmd/gtctl/connect/connect.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2023 Greptime Team -// -// 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 connect - -import ( - "errors" - "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/connect/mysql" - "github.com/GreptimeTeam/gtctl/pkg/logger" - "github.com/spf13/cobra" - "strings" -) - -const ( - SplitSeparator = "://" - MySQL = "mysql" -) - -func NewConnectCommand(l logger.Logger) *cobra.Command { - cmd := &cobra.Command{ - Use: "connect", - Short: "Connect to a GreptimeDB cluster", - Long: `Connect to a GreptimeDB cluster`, - RunE: connectCommand, - } - return cmd -} - -func connectCommand(cmd *cobra.Command, args []string) error { - s := args[0] - split := strings.Split(s, SplitSeparator) - if len(split) != 2 { - return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") - } - prefix := split[0] - switch prefix { - case MySQL: - return mysql.ConnectCommand(cmd, args) - default: - return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") - } -} diff --git a/pkg/cmd/gtctl/connect/mysql/mysql.go b/pkg/cmd/gtctl/connect/mysql/mysql.go deleted file mode 100644 index 3b588658..00000000 --- a/pkg/cmd/gtctl/connect/mysql/mysql.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2023 Greptime Team -// -// 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 mysql - -import ( - "errors" - "github.com/spf13/cobra" - "log" - "os" - "os/exec" - "strings" -) - -const ( - UpSeparator = ":" - AtSeparator = "@" - ArgHost = "-h" - ArgPort = "-P" - ArgUser = "-u" - ArgPassword = "-p" - MySQL = "mysql" - SplitSeparator = "://" -) - -func ConnectCommand(cmd *cobra.Command, args []string) error { - s := args[0] - suffix := strings.Split(s, SplitSeparator)[1] - split := strings.Split(suffix, AtSeparator) - if len(split) != 2 { - return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") - } - up := strings.Split(split[0], UpSeparator) - if len(up) != 2 { - return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") - } - hp := strings.Split(split[1], UpSeparator) - if len(hp) != 2 { - return errors.New("invalid argument, you can try gtctl connect mysql://user:password@host:port") - } - user := up[0] - password := up[1] - host := hp[0] - port := hp[1] - return Connect(user, password, host, port) -} - -func Connect(user, password, host, port string) error { - cmd := exec.Command(MySQL, ArgHost, host, ArgPort, port, ArgUser, user, ArgPassword, password) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - err := cmd.Start() - if err != nil { - log.Fatal(err) - } - err = cmd.Wait() - return err -} diff --git a/pkg/cmd/gtctl/root.go b/pkg/cmd/gtctl/root.go index 6ab76b21..9df8cf2d 100644 --- a/pkg/cmd/gtctl/root.go +++ b/pkg/cmd/gtctl/root.go @@ -16,7 +16,6 @@ package gtctl import ( "fmt" - "github.com/GreptimeTeam/gtctl/pkg/cmd/gtctl/connect" "os" "github.com/spf13/cobra" @@ -63,7 +62,6 @@ func NewRootCommand() *cobra.Command { // Add all top level subcommands. cmd.AddCommand(version.NewVersionCommand(l)) cmd.AddCommand(cluster.NewClusterCommand(l)) - cmd.AddCommand(connect.NewConnectCommand(l)) return cmd } From 85da549e4adc7f7ee6b4891372d367b6f89757db Mon Sep 17 00:00:00 2001 From: sjcsjc123 <1401189096@qq.com> Date: Mon, 24 Jul 2023 20:31:39 +0800 Subject: [PATCH 09/10] fix bug with golangci-lint #3 --- pkg/cmd/gtctl/cluster/connect/mysql/mysql.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go b/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go index ba4188bc..aadf5187 100644 --- a/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go +++ b/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go @@ -48,10 +48,13 @@ func connect(host, port, clusterName string, l logger.Logger) error { l.Errorf("Error starting port-forwarding: %v", err) return err } + waitGroup.Add(1) go func() { - waitGroup.Add(1) defer waitGroup.Done() if err = cmd.Wait(); err != nil { + if err != nil && err.Error() != "signal: killed" { + l.V(1).Info("Shutting down port-forwarding successfully") + } } }() for { From b8fe51f5c01e277cdb4c606d0fd8f086d3f46ae3 Mon Sep 17 00:00:00 2001 From: sjcsjc123 <1401189096@qq.com> Date: Tue, 25 Jul 2023 21:05:47 +0800 Subject: [PATCH 10/10] improve code style #3 --- pkg/cmd/gtctl/cluster/connect/connect.go | 19 +++++---- pkg/cmd/gtctl/cluster/connect/mysql/mysql.go | 43 ++++++++++++++------ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/pkg/cmd/gtctl/cluster/connect/connect.go b/pkg/cmd/gtctl/cluster/connect/connect.go index 49bc824f..ae8c4bf0 100644 --- a/pkg/cmd/gtctl/cluster/connect/connect.go +++ b/pkg/cmd/gtctl/cluster/connect/connect.go @@ -17,7 +17,6 @@ package connect import ( "context" "fmt" - "strings" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" @@ -30,8 +29,14 @@ import ( "github.com/GreptimeTeam/gtctl/pkg/logger" ) +const ( + connectionProtocolMySQL = "mysql" + connectionProtocolPostgres = "pg" +) + type getClusterCliOptions struct { Namespace string + Protocol string } func NewConnectCommand(l logger.Logger) *cobra.Command { @@ -69,17 +74,16 @@ func NewConnectCommand(l logger.Logger) *cobra.Command { if !ok { return fmt.Errorf("invalid cluster type") } - dbType := cmd.Flag("p") - switch strings.ToLower(dbType.Value.String()) { - case "mysql": + switch options.Protocol { + case connectionProtocolMySQL: err := mysql.ConnectCommand(rawCluster, l) if err != nil { - _ = fmt.Errorf("error connecting to mysql: %v", err) + return fmt.Errorf("error connecting to mysql: %v", err) } - case "pg": + case connectionProtocolPostgres: err := pg.ConnectCommand(rawCluster, l) if err != nil { - _ = fmt.Errorf("error connecting to postgres: %v", err) + return fmt.Errorf("error connecting to postgres: %v", err) } default: return fmt.Errorf("database type not supported") @@ -89,5 +93,6 @@ func NewConnectCommand(l logger.Logger) *cobra.Command { } cmd.Flags().String("p", "mysql", "Specify a database") cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "default", "Namespace of GreptimeDB cluster.") + cmd.Flags().StringVarP(&options.Protocol, "protocol", "p", "mysql", "Specify a database") return cmd } diff --git a/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go b/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go index aadf5187..4f43e02e 100644 --- a/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go +++ b/pkg/cmd/gtctl/cluster/connect/mysql/mysql.go @@ -21,6 +21,7 @@ import ( "os/exec" "strconv" "sync" + "syscall" "github.com/go-sql-driver/mysql" @@ -29,9 +30,14 @@ import ( ) const ( - ArgHost = "-h" - ArgPort = "-P" - MySQL = "mysql" + ArgHost = "-h" + ArgPort = "-P" + MySQL = "mysql" + DefaultAddr = "127.0.0.1" + DefaultNet = "tcp" + PrePort = ":" + Kubectl = "kubectl" + PortForward = "port-forward" ) // ConnectCommand connects to a GreptimeDB cluster @@ -42,7 +48,7 @@ func ConnectCommand(rawCluster *greptimedbclusterv1alpha1.GreptimeDBCluster, l l // connect connects to a GreptimeDB cluster func connect(host, port, clusterName string, l logger.Logger) error { waitGroup := sync.WaitGroup{} - cmd := exec.CommandContext(context.Background(), "kubectl", "port-forward", "-n", "default", "svc/"+clusterName+"-frontend", port+":"+port) + cmd := exec.CommandContext(context.Background(), Kubectl, PortForward, "-n", "default", "svc/"+clusterName+"-frontend", port+PrePort+port) err := cmd.Start() if err != nil { l.Errorf("Error starting port-forwarding: %v", err) @@ -52,22 +58,28 @@ func connect(host, port, clusterName string, l logger.Logger) error { go func() { defer waitGroup.Done() if err = cmd.Wait(); err != nil { - if err != nil && err.Error() != "signal: killed" { - l.V(1).Info("Shutting down port-forwarding successfully") + //exit status 1 + exitError, ok := err.(*exec.ExitError) + if !ok { + l.Errorf("Error waiting for port-forwarding to finish: %v", err) + return + } + if exitError.Sys().(syscall.WaitStatus).ExitStatus() == 1 { + return } } }() for { cfg := mysql.Config{ - Net: "tcp", - Addr: "127.0.0.1:4002", + Net: DefaultNet, + Addr: DefaultAddr + PrePort + port, User: "", Passwd: "", DBName: "", AllowNativePasswords: true, } - db, err := sql.Open("mysql", cfg.FormatDSN()) + db, err := sql.Open(MySQL, cfg.FormatDSN()) if err != nil { continue } @@ -79,13 +91,15 @@ func connect(host, port, clusterName string, l logger.Logger) error { err = db.Close() if err != nil { - l.V(1).Infof("Error closing connection: %v", err) + if err == os.ErrProcessDone { + return nil + } return err } break } - cmd = exec.Command(MySQL, ArgHost, host, ArgPort, port) + cmd = mysqlCommand(port) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin @@ -101,11 +115,16 @@ func connect(host, port, clusterName string, l logger.Logger) error { // gracefully stop port-forwarding err = cmd.Process.Kill() if err != nil { - if err.Error() != "os: process already finished" { + if err == os.ErrProcessDone { l.V(1).Info("Shutting down port-forwarding successfully") + return nil } return err } waitGroup.Wait() return nil } + +func mysqlCommand(port string) *exec.Cmd { + return exec.Command(MySQL, ArgHost, DefaultAddr, ArgPort, port) +}