diff --git a/go.mod b/go.mod index 4d181c93a..c0c95ce79 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.9.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.3 @@ -46,7 +47,7 @@ require ( k8s.io/apimachinery v0.29.3 k8s.io/client-go v0.29.3 k8s.io/kubernetes v1.27.2 - k8s.io/metrics v0.0.0 + k8s.io/metrics v0.29.3 sigs.k8s.io/controller-runtime v0.15.0 ) @@ -144,7 +145,6 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect diff --git a/internal/cli/cmd/install/install.go b/internal/cli/cmd/install/install.go index 205973c7a..f80711958 100644 --- a/internal/cli/cmd/install/install.go +++ b/internal/cli/cmd/install/install.go @@ -20,14 +20,12 @@ import ( "github.com/oceanbase/ob-operator/internal/cli/utils" ) +var componentList = []string{"ob-operator", "ob-dashboard", "local-path-provisioner", "cert-manager", "local-path-provisioner-dev", "ob-operator-dev"} + // NewCmd install the ob-operator and other components func NewCmd() *cobra.Command { o := install.NewInstallOptions() logger := utils.GetDefaultLoggerInstance() - componentList := []string{} - for component := range o.Components { - componentList = append(componentList, component) - } cmd := &cobra.Command{ Use: "install ", Short: "Command for ob-operator and other components installation", @@ -48,6 +46,7 @@ if not specified, install ob-operator and ob-dashboard by default, and cert-mana logger.Println("Install ob-operator and ob-dashboard by default") } for component, version := range o.Components { + logger.Printf("Installing component %s, version %s\n", component, version) if err := o.Install(component, version); err != nil { logger.Fatalln(err) } else { diff --git a/internal/cli/cmd/update/update.go b/internal/cli/cmd/update/update.go index 77a080bbf..08907c9d5 100644 --- a/internal/cli/cmd/update/update.go +++ b/internal/cli/cmd/update/update.go @@ -20,14 +20,12 @@ import ( "github.com/oceanbase/ob-operator/internal/cli/utils" ) +var componentList = []string{"ob-operator", "ob-dashboard", "local-path-provisioner", "cert-manager"} + // NewCmd update the ob-operator and other components func NewCmd() *cobra.Command { o := update.NewUpdateOptions() logger := utils.GetDefaultLoggerInstance() - componentList := []string{} - for component := range o.Components { - componentList = append(componentList, component) - } cmd := &cobra.Command{ Use: "update ", Short: "Command for ob-operator and other components update", @@ -40,15 +38,17 @@ Currently support: - cert-manager: Creates TLS certificates for workloads in Kubernetes and renews the certificates before they expire, ob-operator relies on it for certificate management, which should be installed beforehand. if not specified, update ob-operator and ob-dashboard by default`, - PreRunE: o.Parse, - ValidArgs: componentList, - Args: cobra.MatchAll(cobra.MaximumNArgs(1), cobra.OnlyValidArgs), + PreRunE: o.Parse, + ValidArgs: componentList, + DisableFlagsInUseLine: true, + Args: cobra.MatchAll(cobra.MaximumNArgs(1), cobra.OnlyValidArgs), Run: func(cmd *cobra.Command, args []string) { componentCount := 0 for component, version := range o.Components { if utils.CheckIfComponentExists(component) { componentCount++ - if err := o.Install(component, version); err != nil { + logger.Printf("Updating component %s, version %s\n", component, version) + if err := o.Update(component, version); err != nil { logger.Fatalln(err) } else { logger.Printf("%s update successfully\n", component) diff --git a/internal/cli/utils/conf.go b/internal/cli/config/config.go similarity index 54% rename from internal/cli/utils/conf.go rename to internal/cli/config/config.go index f312e794b..22a0cca31 100644 --- a/internal/cli/utils/conf.go +++ b/internal/cli/config/config.go @@ -11,7 +11,7 @@ EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more details. */ -package utils +package config import ( "bytes" @@ -20,14 +20,15 @@ import ( "github.com/spf13/viper" "github.com/oceanbase/ob-operator/internal/cli/generated/bindata" + "github.com/oceanbase/ob-operator/internal/cli/utils" ) // component config for test -var component_conf = "internal/assets/cli-templates/component_config.yaml" +var confPath = "internal/assets/cli-templates/component_config.yaml" -func GetComponentsConf() map[string]string { +func readComponentConf(path string) map[string]string { components := make(map[string]string) - fileobj, err := bindata.Asset(component_conf) + fileobj, err := bindata.Asset(path) // panic if file not exists if err != nil { panic(fmt.Errorf("Error reading component config file: %v", err)) @@ -42,3 +43,24 @@ func GetComponentsConf() map[string]string { } return components } + +// GetAllComponents returns all the components +func GetAllComponents() map[string]string { + return readComponentConf(confPath) +} + +// GetDefaultComponents returns the default components to be installed +func GetDefaultComponents() map[string]string { + var componentsList []string + components := GetAllComponents() + defaultComponents := make(map[string]string) // Initialize the map + if !utils.CheckIfComponentExists("cert-manager") { + componentsList = []string{"cert-manager", "ob-operator", "ob-dashboard"} + } else { + componentsList = []string{"ob-operator", "ob-dashboard"} + } + for _, component := range componentsList { + defaultComponents[component] = components[component] + } + return defaultComponents +} diff --git a/internal/cli/install/install.go b/internal/cli/install/install.go index b2d6a1da0..fcf09ce42 100644 --- a/internal/cli/install/install.go +++ b/internal/cli/install/install.go @@ -15,135 +15,59 @@ package install import ( "fmt" - "os" - "os/exec" "github.com/spf13/cobra" + "github.com/oceanbase/ob-operator/internal/cli/config" utils "github.com/oceanbase/ob-operator/internal/cli/utils" ) type InstallOptions struct { - version string - Components map[string]string - obUrl string - localPathUrl string + version string + Components map[string]string } +// NewInstallOptions create a new InstallOptions func NewInstallOptions() *InstallOptions { return &InstallOptions{ - Components: utils.GetComponentsConf(), - obUrl: "https://raw.githubusercontent.com/oceanbase/ob-operator/", - localPathUrl: "https://raw.githubusercontent.com/rancher/local-path-provisioner/", + Components: make(map[string]string), } } -func (o *InstallOptions) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVar(&o.version, "version", "", "version of component") -} - func (o *InstallOptions) Parse(_ *cobra.Command, args []string) error { - // if not specified, use default config - if len(args) == 0 { - defaultComponents := o.GetDefaultComponents() - // update Components to default config - o.Components = defaultComponents - return nil - } - name := args[0] - if v, ok := o.Components[name]; ok { + // if specified, use the specified component + if len(args) > 0 { + name := args[0] + components := config.GetAllComponents() + // check if the component is supported + defaultVersion, exist := components[name] + if !exist { + return fmt.Errorf("component `%v` is not supported", name) + } + + // if version is not specified, use the default version if o.version == "" { - o.Components = map[string]string{name: v} + o.Components = map[string]string{name: defaultVersion} } else { o.Components = map[string]string{name: o.version} } - return nil - } - return fmt.Errorf("component `%v` is not supported", name) -} - -// Install component -func (o *InstallOptions) Install(component, version string) error { - cmd, err := o.buildCmd(component, version) - if err != nil { - return err - } - return runCmd(cmd) -} - -// buildCmd build cmd for installation -func (o *InstallOptions) buildCmd(component, version string) (*exec.Cmd, error) { - var cmd *exec.Cmd - var url string - switch component { - case "cert-manager": - componentFile := "cert-manager.yaml" - url = fmt.Sprintf("%s%s/deploy/%s", o.obUrl, version, componentFile) - cmd = exec.Command("kubectl", "apply", "-f", url) - case "ob-operator", "ob-operator-dev": - componentFile := "operator.yaml" - url = fmt.Sprintf("%s%s/deploy/%s", o.obUrl, version, componentFile) - cmd = exec.Command("kubectl", "apply", "-f", url) - case "local-path-provisioner", "local-path-provisioner-dev": - componentFile := "local-path-storage.yaml" - url = fmt.Sprintf("%s%s/deploy/%s", o.localPathUrl, version, componentFile) - cmd = exec.Command("kubectl", "apply", "-f", url) - case "ob-dashboard": - if err := addHelmRepo(); err != nil { - return nil, err - } - if err := updateHelmRepo(); err != nil { - return nil, err - } - versionFlag := fmt.Sprintf("--version=%s", version) - cmd = exec.Command("helm", "install", "oceanbase-dashboard", "ob-operator/oceanbase-dashboard", versionFlag) - default: - return nil, fmt.Errorf("unknown component: %s", component) - } - return cmd, nil -} - -func (o *InstallOptions) GetDefaultComponents() map[string]string { - defaultComponents := make(map[string]string) // Initialize the map - var componentsList []string - if !utils.CheckIfComponentExists("cert-manager") { - componentsList = []string{"cert-manager", "ob-operator", "ob-dashboard"} } else { - componentsList = []string{"ob-operator", "ob-dashboard"} - } - for _, component := range componentsList { - defaultComponents[component] = o.Components[component] - } - return defaultComponents -} - -// addHelmRepo add ob-operator helm repo -func addHelmRepo() error { - cmdAddRepo := exec.Command("helm", "repo", "add", "ob-operator", "https://oceanbase.github.io/ob-operator/") - output, err := cmdAddRepo.CombinedOutput() - if err != nil { - return fmt.Errorf("adding repo failed: %s, %s", err, output) + // if no component is specified, install default components + defaultComponents := config.GetDefaultComponents() + o.Components = defaultComponents } return nil } -// updateHelmRepo update ob-operator helm repo -func updateHelmRepo() error { - cmdUpdateRepo := exec.Command("helm", "repo", "update") - output, err := cmdUpdateRepo.CombinedOutput() +func (o *InstallOptions) Install(component, version string) error { + cmd, err := utils.BuildCmd(component, version) if err != nil { - return fmt.Errorf("updating repo failed: %s, %s", err, output) + return err } - return nil + return utils.RunCmd(cmd) } -// runCmd run cmd for components' installation -func runCmd(cmd *exec.Cmd) error { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - return fmt.Errorf("%s command failed with error: %v", cmd.String(), err) - } - return nil +// AddFlags add flags to install command +func (o *InstallOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.version, "version", "", "version of component") } diff --git a/internal/cli/update/update.go b/internal/cli/update/update.go index 26375d51c..28ae99e74 100644 --- a/internal/cli/update/update.go +++ b/internal/cli/update/update.go @@ -18,31 +18,46 @@ import ( "github.com/spf13/cobra" - "github.com/oceanbase/ob-operator/internal/cli/install" + "github.com/oceanbase/ob-operator/internal/cli/config" + "github.com/oceanbase/ob-operator/internal/cli/utils" ) type UpdateOptions struct { - install.InstallOptions + Components map[string]string } +// NewUpdateOptions create a new UpdateOptions func NewUpdateOptions() *UpdateOptions { return &UpdateOptions{ - InstallOptions: *install.NewInstallOptions(), + Components: make(map[string]string), } } func (o *UpdateOptions) Parse(_ *cobra.Command, args []string) error { - // if not specified, use default config - if len(args) == 0 { - defaultComponents := o.InstallOptions.GetDefaultComponents() - // update Components to default config - o.InstallOptions.Components = defaultComponents - return nil + // if specified, use the specified component + if len(args) > 0 { + name := args[0] + components := config.GetAllComponents() + // check if the component is supported + defaultVersion, exist := components[name] + + // check if the component is supported + if !exist { + return fmt.Errorf("component %s is not supported", name) + } + o.Components = map[string]string{name: defaultVersion} + } else { + // if no component is specified, update default components + defaultComponents := config.GetDefaultComponents() + o.Components = defaultComponents } - name := args[0] - if v, ok := o.InstallOptions.Components[name]; ok { - o.InstallOptions.Components = map[string]string{name: v} - return nil + return nil +} + +func (o *UpdateOptions) Update(component, version string) error { + cmd, err := utils.BuildCmd(component, version) + if err != nil { + return err } - return fmt.Errorf("component %s is not supported", name) + return utils.RunCmd(cmd) } diff --git a/internal/cli/utils/checker.go b/internal/cli/utils/checker.go index abfd81293..df3d32c64 100644 --- a/internal/cli/utils/checker.go +++ b/internal/cli/utils/checker.go @@ -116,11 +116,11 @@ func CheckIfComponentExists(component string) bool { switch component { case "cert-manager": return checkIfResourceExists(certManagerCheckCmd, certManagerResources...) - case "ob-operator": + case "ob-operator", "ob-operator-dev": return checkIfResourceExists(operatorCheckCmd, operatorResources...) case "ob-dashboard": return checkIfResourceExists(dashboardCheckCmd, dashboardResources) - case "local-path-provisioner": + case "local-path-provisioner", "local-path-provisioner-dev": return checkIfResourceExists(localPathProvisionerCheckCmd, localPathProvisionerResources) default: return false diff --git a/internal/cli/utils/cmd_runner.go b/internal/cli/utils/cmd_runner.go new file mode 100644 index 000000000..85ab4d8bf --- /dev/null +++ b/internal/cli/utils/cmd_runner.go @@ -0,0 +1,90 @@ +/* +Copyright (c) 2024 OceanBase +ob-operator is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan PSL v2. +You may obtain a copy of Mulan PSL v2 at: + + http://license.coscl.org.cn/MulanPSL2 + +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. +*/ +package utils + +import ( + "fmt" + "os" + "os/exec" +) + +// AddHelmRepo add ob-operator helm repo +func AddHelmRepo() error { + repoURL := "https://oceanbase.github.io/ob-operator/" + cmdAddRepo := exec.Command("helm", "repo", "add", "ob-operator", repoURL) + if err := RunCmd(cmdAddRepo); err != nil { + return fmt.Errorf("adding repo failed: %s", err) + } + return nil +} + +// UpdateHelmRepo update ob-operator helm repo +func UpdateHelmRepo() error { + cmdUpdateRepo := exec.Command("helm", "repo", "update") + if err := RunCmd(cmdUpdateRepo); err != nil { + return fmt.Errorf("updating repo failed: %s", err) + } + return nil +} + +// RunCmd runs the command and prints the output to stdout +func RunCmd(cmd *exec.Cmd) error { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("%s command failed with error: %v", cmd.String(), err) + } + return nil +} + +// BuildCmd builds the command based on the component and version +func BuildCmd(component, version string) (*exec.Cmd, error) { + var cmd *exec.Cmd + var ( + obURL = "https://raw.githubusercontent.com/oceanbase/ob-operator/" + localPathURL = "https://raw.githubusercontent.com/rancher/local-path-provisioner/" + dashboardRepo = "ob-operator/oceanbase-dashboard" + ) + + switch component { + case "cert-manager": + componentFile := "cert-manager.yaml" + url := fmt.Sprintf("%s%s/deploy/%s", obURL, version, componentFile) + cmd = exec.Command("kubectl", "apply", "-f", url) + case "ob-operator", "ob-operator-dev": + componentFile := "operator.yaml" + url := fmt.Sprintf("%s%s/deploy/%s", obURL, version, componentFile) + cmd = exec.Command("kubectl", "apply", "-f", url) + case "local-path-provisioner", "local-path-provisioner-dev": + componentFile := "local-path-storage.yaml" + url := fmt.Sprintf("%s%s/deploy/%s", localPathURL, version, componentFile) + cmd = exec.Command("kubectl", "apply", "-f", url) + case "ob-dashboard": + versionFlag := fmt.Sprintf("--version=%s", version) + if err := AddHelmRepo(); err != nil { + return nil, err + } + if err := UpdateHelmRepo(); err != nil { + return nil, err + } + if !CheckIfComponentExists("ob-dashboard") { + cmd = exec.Command("helm", "install", "oceanbase-dashboard", dashboardRepo, versionFlag) + } else { + cmd = exec.Command("helm", "upgrade", "oceanbase-dashboard", dashboardRepo, versionFlag) + } + default: + return nil, fmt.Errorf("unknown component: %s", component) + } + return cmd, nil +} diff --git a/internal/cli/utils/cmd_runner_test.go b/internal/cli/utils/cmd_runner_test.go new file mode 100644 index 000000000..66f8382ed --- /dev/null +++ b/internal/cli/utils/cmd_runner_test.go @@ -0,0 +1,77 @@ +/* +Copyright (c) 2024 OceanBase +ob-operator is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan PSL v2. +You may obtain a copy of Mulan PSL v2 at: + + http://license.coscl.org.cn/MulanPSL2 + +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. +*/ +package utils_test + +import ( + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/oceanbase/ob-operator/internal/cli/utils" +) + +func TestAddHelmRepo(t *testing.T) { + cmd := exec.Command("echo", "repo added") + _, err := cmd.CombinedOutput() + if err != nil { + t.Errorf("failed to run command: %v", err) + } + + err = utils.AddHelmRepo() + assert.NoError(t, err) + assert.NotNil(t, cmd) +} + +func TestUpdateHelmRepo(t *testing.T) { + cmd := exec.Command("echo", "repo updated") + _, err := cmd.CombinedOutput() + if err != nil { + t.Errorf("failed to run command: %v", err) + } + + err = utils.UpdateHelmRepo() + assert.NoError(t, err) + assert.NotNil(t, cmd) +} + +func TestBuildCmd(t *testing.T) { + cmd, err := utils.BuildCmd("cert-manager", "2.2.2_release") + assert.NoError(t, err) + assert.NotNil(t, cmd) + + cmd, err = utils.BuildCmd("ob-operator", "2.3.0") + assert.NoError(t, err) + assert.NotNil(t, cmd) + + cmd, err = utils.BuildCmd("fake-component", "1.0.0") + assert.Error(t, err) + assert.Nil(t, cmd) +} + +func TestRunCmd(t *testing.T) { + cmd := exec.Command("echo", "running command") + err := utils.RunCmd(cmd) + assert.NoError(t, err) + + cmd = exec.Command("sh", "-c", "exit 1") + err = utils.RunCmd(cmd) + assert.Error(t, err) + + cmd, err = utils.BuildCmd("cert-manager", "2.2.2_release") + assert.NoError(t, err) + err = utils.RunCmd(cmd) + assert.NoError(t, err) + +}