diff --git a/internal/cli/cluster/create.go b/internal/cli/cluster/create.go index a9dff9fa1..ed5356b6f 100644 --- a/internal/cli/cluster/create.go +++ b/internal/cli/cluster/create.go @@ -14,6 +14,7 @@ See the Mulan PSL v2 for more details. package cluster import ( + "errors" "fmt" apitypes "github.com/oceanbase/ob-operator/api/types" @@ -28,13 +29,14 @@ import ( apiresource "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/oceanbase/ob-operator/internal/cli/generic" utils "github.com/oceanbase/ob-operator/internal/cli/utils" modelcommon "github.com/oceanbase/ob-operator/internal/dashboard/model/common" param "github.com/oceanbase/ob-operator/internal/dashboard/model/param" ) type CreateOptions struct { - ResourceOptions + generic.ResourceOption ClusterName string `json:"clusterName"` ClusterId int64 `json:"clusterId"` RootPassword string `json:"rootPassword"` @@ -44,6 +46,7 @@ type CreateOptions struct { Parameters []modelcommon.KVPair `json:"parameters"` BackupVolume *param.NFSVolumeSpec `json:"backupVolume"` Zones map[string]string `json:"zones"` + KvParameters map[string]string `json:"kvParameters"` Mode string `json:"mode"` } @@ -59,6 +62,9 @@ func NewCreateOptions() *CreateOptions { } func (o *CreateOptions) Validate() error { + if o.Namespace == "" { + return errors.New("namespace is not specified") + } if !utils.CheckPassword(o.RootPassword) { return fmt.Errorf("Password is not secure, must contain at least 2 uppercase and lowercase letters, numbers and special characters") } @@ -73,6 +79,11 @@ func (o *CreateOptions) Parse(_ *cobra.Command, args []string) error { if err != nil { return err } + parameters, err := utils.MapParameters(o.KvParameters) + if err != nil { + return err + } + o.Parameters = parameters o.Topology = topology o.Name = args[0] return nil @@ -81,7 +92,7 @@ func (o *CreateOptions) Parse(_ *cobra.Command, args []string) error { func (o *CreateOptions) Complete() error { // if not specific id, using timestamp if o.ClusterId == 0 { - o.ClusterId = utils.GenerateClusterId() + o.ClusterId = utils.GenerateClusterID() } // if not specific password, using random password, range [8,32] if o.RootPassword == "" { @@ -93,62 +104,6 @@ func (o *CreateOptions) Complete() error { return nil } -func (o *CreateOptions) AddFlags(cmd *cobra.Command) { - // Add base and specific feature flags, Only support observer and zone config - o.AddBaseFlags(cmd) - o.AddObserverFlags(cmd) - o.AddZoneFlags(cmd) -} - -// AddZoneFlags adds the zone-related flags to the command. -func (o *CreateOptions) AddZoneFlags(cmd *cobra.Command) { - zoneFlags := pflag.NewFlagSet("zone", pflag.ContinueOnError) - zoneFlags.StringToStringVarP(&o.Zones, "zones", "z", map[string]string{"z1": "1"}, "The zones of the cluster in the format 'zone=value', multiple values can be provided separated by commas") - cmd.Flags().AddFlagSet(zoneFlags) -} - -// AddBaseFlags adds the base flags to the command. -func (o *CreateOptions) AddBaseFlags(cmd *cobra.Command) { - baseFlags := cmd.Flags() - baseFlags.StringVar(&o.Name, "name", "", "The name in k8s, if not specified, use cluster name") - baseFlags.StringVar(&o.Namespace, "namespace", "default", "The namespace of the cluster") - baseFlags.Int64Var(&o.ClusterId, "id", 0, "The id of the cluster") - baseFlags.StringVar(&o.RootPassword, "root-password", "", "The root password of the cluster") - baseFlags.StringVar(&o.Mode, "mode", "", "The mode of the cluster") -} - -// AddObserverFlags adds the observer-related flags to the command. -func (o *CreateOptions) AddObserverFlags(cmd *cobra.Command) { - observerFlags := pflag.NewFlagSet("observer", pflag.ContinueOnError) - observerFlags.StringVar(&o.OBServer.Image, "image", "oceanbase/oceanbase-cloud-native:4.2.1.6-106000012024042515", "The image of the observer") - observerFlags.Int64Var(&o.OBServer.Resource.Cpu, "cpu", 2, "The cpu of the observer") - observerFlags.Int64Var(&o.OBServer.Resource.MemoryGB, "memory", 10, "The memory of the observer") - observerFlags.StringVar(&o.OBServer.Storage.Data.StorageClass, "data-storage-class", "local-path", "The storage class of the data storage") - observerFlags.StringVar(&o.OBServer.Storage.RedoLog.StorageClass, "redo-log-storage-class", "local-path", "The storage class of the redo log storage") - observerFlags.StringVar(&o.OBServer.Storage.Log.StorageClass, "log-storage-class", "local-path", "The storage class of the log storage") - observerFlags.Int64Var(&o.OBServer.Storage.Data.SizeGB, "data-storage-size", 50, "The size of the data storage") - observerFlags.Int64Var(&o.OBServer.Storage.RedoLog.SizeGB, "redo-log-storage-size", 50, "The size of the redo log storage") - observerFlags.Int64Var(&o.OBServer.Storage.Log.SizeGB, "log-storage-size", 20, "The size of the log storage") - cmd.Flags().AddFlagSet(observerFlags) -} - -// AddMonitorFlags adds the monitor-related flags to the command. -func (o *CreateOptions) AddMonitorFlags(cmd *cobra.Command) { - monitorFlags := pflag.NewFlagSet("monitor", pflag.ContinueOnError) - monitorFlags.StringVar(&o.Monitor.Image, "monitor-image", "oceanbase/obagent:4.2.1-100000092023101717", "The image of the monitor") - monitorFlags.Int64Var(&o.Monitor.Resource.Cpu, "monitor-cpu", 1, "The cpu of the monitor") - monitorFlags.Int64Var(&o.Monitor.Resource.MemoryGB, "monitor-memory", 1, "The memory of the monitor") - cmd.Flags().AddFlagSet(monitorFlags) -} - -// AddBackupVolumeFlags adds the backup-volume-related flags to the command. -func (o *CreateOptions) AddBackupVolumeFlags(cmd *cobra.Command) { - backupVolumeFlags := pflag.NewFlagSet("backup-volume", pflag.ContinueOnError) - backupVolumeFlags.StringVar(&o.BackupVolume.Address, "backup-storage-class", "local-path", "The storage class of the backup storage") - backupVolumeFlags.StringVar(&o.BackupVolume.Path, "backup-storage-size", "/opt/nfs", "The size of the backup storage") - cmd.Flags().AddFlagSet(backupVolumeFlags) -} - func buildOBServerTemplate(observerSpec *param.OBServerSpec) *apitypes.OBServerTemplate { if observerSpec == nil { return nil @@ -334,3 +289,67 @@ func CreateOBClusterInstance(param *CreateOptions) *v1alpha1.OBCluster { } return obcluster } + +// AddFlags adds base and specific feature flags, Only support observer and zone config +func (o *CreateOptions) AddFlags(cmd *cobra.Command) { + o.AddBaseFlags(cmd) + o.AddObserverFlags(cmd) + o.AddZoneFlags(cmd) + o.AddParameterFlags(cmd) +} + +// AddZoneFlags adds the zone-related flags to the command. +func (o *CreateOptions) AddZoneFlags(cmd *cobra.Command) { + zoneFlags := pflag.NewFlagSet(FLAGSET_ZONE, pflag.ContinueOnError) + zoneFlags.StringToStringVarP(&o.Zones, FLAG_ZONES, "z", map[string]string{"z1": "1"}, "The zones of the cluster in the format 'Zone=Replica', multiple values can be provided separated by commas") + cmd.Flags().AddFlagSet(zoneFlags) +} + +// AddBaseFlags adds the base flags to the command. +func (o *CreateOptions) AddBaseFlags(cmd *cobra.Command) { + baseFlags := cmd.Flags() + baseFlags.StringVarP(&o.ClusterName, FLAG_CLUSTER_NAME, "n", "", "Cluster name, if not specified, use resource name in k8s instead") + baseFlags.StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "The namespace of the cluster") + baseFlags.Int64Var(&o.ClusterId, FLAG_CLUSTER_ID, 0, "The id of the cluster") + baseFlags.StringVarP(&o.RootPassword, FLAG_ROOTPASSWD, "p", "", "The root password of the cluster") + baseFlags.StringVar(&o.Mode, FLAG_MODE, "", "The mode of the cluster") +} + +// AddObserverFlags adds the observer-related flags to the command. +func (o *CreateOptions) AddObserverFlags(cmd *cobra.Command) { + observerFlags := pflag.NewFlagSet(FLAGSET_OBSERVER, pflag.ContinueOnError) + observerFlags.StringVar(&o.OBServer.Image, FLAG_OBSERVER_IMAGE, "oceanbase/oceanbase-cloud-native:4.2.1.6-106000012024042515", "The image of the observer") + observerFlags.Int64Var(&o.OBServer.Resource.Cpu, FLAG_OBSERVER_CPU, 2, "The cpu of the observer") + observerFlags.Int64Var(&o.OBServer.Resource.MemoryGB, FLAG_MONITOR_MEMORY, 10, "The memory of the observer") + observerFlags.StringVar(&o.OBServer.Storage.Data.StorageClass, FLAG_DATA_STORAGE_CLASS, "local-path", "The storage class of the data storage") + observerFlags.StringVar(&o.OBServer.Storage.RedoLog.StorageClass, FLAG_REDO_LOG_STORAGE_CLASS, "local-path", "The storage class of the redo log storage") + observerFlags.StringVar(&o.OBServer.Storage.Log.StorageClass, FLAG_LOG_STORAGE_CLASS, "local-path", "The storage class of the log storage") + observerFlags.Int64Var(&o.OBServer.Storage.Data.SizeGB, FLAG_DATA_STORAGE_SIZE, 50, "The size of the data storage") + observerFlags.Int64Var(&o.OBServer.Storage.RedoLog.SizeGB, FLAG_REDO_LOG_STORAGE_SIZE, 50, "The size of the redo log storage") + observerFlags.Int64Var(&o.OBServer.Storage.Log.SizeGB, FLAG_LOG_STORAGE_SIZE, 20, "The size of the log storage") + cmd.Flags().AddFlagSet(observerFlags) +} + +// AddMonitorFlags adds the monitor-related flags to the command. +func (o *CreateOptions) AddMonitorFlags(cmd *cobra.Command) { + monitorFlags := pflag.NewFlagSet(FLAGSET_MONITOR, pflag.ContinueOnError) + monitorFlags.StringVar(&o.Monitor.Image, FLAG_MONITOR_IMAGE, "oceanbase/obagent:4.2.1-100000092023101717", "The image of the monitor") + monitorFlags.Int64Var(&o.Monitor.Resource.Cpu, FLAG_MONITOR_CPU, 1, "The cpu of the monitor") + monitorFlags.Int64Var(&o.Monitor.Resource.MemoryGB, FLAG_MONITOR_MEMORY, 1, "The memory of the monitor") + cmd.Flags().AddFlagSet(monitorFlags) +} + +// AddBackupVolumeFlags adds the backup-volume-related flags to the command. +func (o *CreateOptions) AddBackupVolumeFlags(cmd *cobra.Command) { + backupVolumeFlags := pflag.NewFlagSet(FLAGSET_BACKUP_VOLUME, pflag.ContinueOnError) + backupVolumeFlags.StringVar(&o.BackupVolume.Address, FLAG_BACKUP_ADDRESS, "local-path", "The storage class of the backup storage") + backupVolumeFlags.StringVar(&o.BackupVolume.Path, FLAG_BACKUP_PATH, "/opt/nfs", "The size of the backup storage") + cmd.Flags().AddFlagSet(backupVolumeFlags) +} + +// AddParameterFlags adds the parameter-related flags, e.g. __min_full_resource_pool_memory, to the command +func (o *CreateOptions) AddParameterFlags(cmd *cobra.Command) { + parameterFlags := pflag.NewFlagSet(FLAGSET_PARAMETERS, pflag.ContinueOnError) + parameterFlags.StringToStringVar(&o.KvParameters, FLAG_PARAMETERS, map[string]string{"__min_full_resource_pool_memory": "2147483648", "system_memory": "1G"}, "Other parameter settings in obcluster, e.g., __min_full_resource_pool_memory") + cmd.Flags().AddFlagSet(parameterFlags) +} diff --git a/internal/cli/cluster/delete.go b/internal/cli/cluster/delete.go index 8ec817346..6a8068486 100644 --- a/internal/cli/cluster/delete.go +++ b/internal/cli/cluster/delete.go @@ -13,10 +13,21 @@ See the Mulan PSL v2 for more details. */ package cluster +import ( + "github.com/spf13/cobra" + + "github.com/oceanbase/ob-operator/internal/cli/generic" +) + type DeleteOptions struct { - ResourceOptions + generic.ResourceOption } func NewDeleteOptions() *DeleteOptions { return &DeleteOptions{} } + +// AddFlags add basic flags for cluster management +func (o *DeleteOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of ob cluster") +} diff --git a/internal/cli/cluster/enter.go b/internal/cli/cluster/enter.go new file mode 100644 index 000000000..2072b69cc --- /dev/null +++ b/internal/cli/cluster/enter.go @@ -0,0 +1,56 @@ +/* +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 cluster + +const ( + // Flagsets for cluster + FLAGSET_ZONE = "zone" + FLAGSET_OBSERVER = "observer" + FLAGSET_MONITOR = "monitor" + FLAGSET_BACKUP_VOLUME = "backup-volume" + FLAGSET_PARAMETERS = "parameters" + + // Flags for all the commands in cluster management + FLAG_CLUSTER_NAME = "cluster-name" + FLAG_NAMESPACE = "namespace" + FLAG_CLUSTER_ID = "id" + FLAG_ROOTPASSWD = "root-password" + FLAG_MODE = "mode" + + // Flags for zone-related options + FLAG_ZONES = "zones" + + // Flags for observer-related options + FLAG_OBSERVER_IMAGE = "image" + FLAG_OBSERVER_CPU = "cpu" + FLAG_OBSERVER_MEMORY = "memory" + FLAG_DATA_STORAGE_CLASS = "data-storage-class" + FLAG_REDO_LOG_STORAGE_CLASS = "redo-log-storage-class" + FLAG_LOG_STORAGE_CLASS = "log-storage-class" + FLAG_DATA_STORAGE_SIZE = "data-storage-size" + FLAG_REDO_LOG_STORAGE_SIZE = "redo-log-storage-size" + FLAG_LOG_STORAGE_SIZE = "log-storage-size" + + // Flags for monitor-related options + FLAG_MONITOR_IMAGE = "monitor-image" + FLAG_MONITOR_CPU = "monitor-cpu" + FLAG_MONITOR_MEMORY = "monitor-memory" + + // Flags for backup-volume-related options + FLAG_BACKUP_ADDRESS = "backup-storage-address" + FLAG_BACKUP_PATH = "backup-storage-path" + + // Flags for parameter-related options + FLAG_PARAMETERS = "parameters" +) diff --git a/internal/cli/cluster/scale.go b/internal/cli/cluster/scale.go index 85e801a6a..49be3dce5 100644 --- a/internal/cli/cluster/scale.go +++ b/internal/cli/cluster/scale.go @@ -23,13 +23,14 @@ import ( apiconst "github.com/oceanbase/ob-operator/api/constants" apitypes "github.com/oceanbase/ob-operator/api/types" "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" utils "github.com/oceanbase/ob-operator/internal/cli/utils" oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" param "github.com/oceanbase/ob-operator/internal/dashboard/model/param" ) type ScaleOptions struct { - ResourceOptions + generic.ResourceOption Zones map[string]string `json:"zones"` Topology []param.ZoneTopology `json:"topology"` OldTopology []apitypes.OBZoneTopology `json:"oldTopology"` @@ -138,7 +139,7 @@ func (o *ScaleOptions) Validate() error { if !found { typeAdd = true } - if typeDelete && zoneNum-deleteNum < maxDeleteNum { + if typeDelete && deleteNum > maxDeleteNum { return fmt.Errorf("Obcluster has %d Zones, can only delete %d zones", zoneNum, maxDeleteNum) } } @@ -156,7 +157,7 @@ func (o *ScaleOptions) Validate() error { o.ScaleType = "addZones" } if trueCount > 1 { - return fmt.Errorf("Only one type of scale is allower at a time") + return fmt.Errorf("Only one type of scale is allowed at a time") } if trueCount == 0 { return fmt.Errorf("No scale type specified") @@ -166,6 +167,6 @@ func (o *ScaleOptions) Validate() error { // Add Flags for scale options func (o *ScaleOptions) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVar(&o.Namespace, "namespace", "default", "namespace of ob cluster") - cmd.Flags().StringToStringVar(&o.Zones, "zones", nil, "zone of ob cluster") + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of ob cluster") + cmd.Flags().StringToStringVar(&o.Zones, FLAG_ZONES, nil, "zone of ob cluster") } diff --git a/internal/cli/cluster/show.go b/internal/cli/cluster/show.go index 6dbcc21f2..bed240cd6 100644 --- a/internal/cli/cluster/show.go +++ b/internal/cli/cluster/show.go @@ -13,10 +13,21 @@ See the Mulan PSL v2 for more details. */ package cluster +import ( + "github.com/spf13/cobra" + + "github.com/oceanbase/ob-operator/internal/cli/generic" +) + type ShowOptions struct { - ResourceOptions + generic.ResourceOption } func NewShowOptions() *ShowOptions { return &ShowOptions{} } + +// AddFlags add basic flags for cluster management +func (o *ShowOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of ob cluster") +} diff --git a/internal/cli/cluster/update.go b/internal/cli/cluster/update.go index 75906a93b..73d068398 100644 --- a/internal/cli/cluster/update.go +++ b/internal/cli/cluster/update.go @@ -24,6 +24,7 @@ import ( apiconst "github.com/oceanbase/ob-operator/api/constants" "github.com/oceanbase/ob-operator/api/types" "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" "github.com/oceanbase/ob-operator/internal/dashboard/business/constant" "github.com/oceanbase/ob-operator/internal/dashboard/model/common" @@ -31,7 +32,7 @@ import ( ) type UpdateOptions struct { - ResourceOptions + generic.ResourceOption Resource common.ResourceSpec `json:"resource"` Storage *param.OBServerStorageSpec `json:"storage"` UpdateType string `json:"updateType"` @@ -44,8 +45,8 @@ func NewUpdateOptions() *UpdateOptions { } } -// GetUpdateOperations creates update opertaions -func GetUpdateOperations(o *UpdateOptions) *v1alpha1.OBClusterOperation { +// GetUpdateOperation creates update opertaions +func GetUpdateOperation(o *UpdateOptions) *v1alpha1.OBClusterOperation { updateOp := &v1alpha1.OBClusterOperation{ ObjectMeta: v1.ObjectMeta{ Name: o.Name + "-update-" + rand.String(6), @@ -127,13 +128,13 @@ func (o *UpdateOptions) Complete() error { // AddFlags for update options func (o *UpdateOptions) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVar(&o.Namespace, "namespace", "default", "namespace of ob cluster") - cmd.Flags().Int64Var(&o.Resource.Cpu, "cpu", 0, "The cpu of the observer") - cmd.Flags().Int64Var(&o.Resource.MemoryGB, "memory", 0, "The memory of the observer") - cmd.Flags().StringVar(&o.Storage.Data.StorageClass, "data-storage-class", "", "The storage class of the data storage") - cmd.Flags().StringVar(&o.Storage.RedoLog.StorageClass, "redo-log-storage-class", "", "The storage class of the redo log storage") - cmd.Flags().StringVar(&o.Storage.Log.StorageClass, "log-storage-class", "", "The storage class of the log storage") - cmd.Flags().Int64Var(&o.Storage.Data.SizeGB, "data-storage-size", 0, "The size of the data storage") - cmd.Flags().Int64Var(&o.Storage.RedoLog.SizeGB, "redo-log-storage-size", 0, "The size of the redo log storage") - cmd.Flags().Int64Var(&o.Storage.Log.SizeGB, "log-storage-size", 0, "The size of the log storage") + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of ob cluster") + cmd.Flags().Int64Var(&o.Resource.Cpu, FLAG_OBSERVER_CPU, 0, "The cpu of the observer") + cmd.Flags().Int64Var(&o.Resource.MemoryGB, FLAG_MONITOR_MEMORY, 0, "The memory of the observer") + cmd.Flags().StringVar(&o.Storage.Data.StorageClass, FLAG_DATA_STORAGE_CLASS, "", "The storage class of the data storage") + cmd.Flags().StringVar(&o.Storage.RedoLog.StorageClass, FLAG_REDO_LOG_STORAGE_CLASS, "", "The storage class of the redo log storage") + cmd.Flags().StringVar(&o.Storage.Log.StorageClass, FLAG_LOG_STORAGE_CLASS, "", "The storage class of the log storage") + cmd.Flags().Int64Var(&o.Storage.Data.SizeGB, FLAG_DATA_STORAGE_SIZE, 0, "The size of the data storage") + cmd.Flags().Int64Var(&o.Storage.RedoLog.SizeGB, FLAG_REDO_LOG_STORAGE_SIZE, 0, "The size of the redo log storage") + cmd.Flags().Int64Var(&o.Storage.Log.SizeGB, FLAG_LOG_STORAGE_SIZE, 0, "The size of the log storage") } diff --git a/internal/cli/cluster/upgrade.go b/internal/cli/cluster/upgrade.go index 3198979f9..f8a6e606b 100644 --- a/internal/cli/cluster/upgrade.go +++ b/internal/cli/cluster/upgrade.go @@ -22,11 +22,12 @@ import ( apiconst "github.com/oceanbase/ob-operator/api/constants" "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" ) type UpgradeOptions struct { - ResourceOptions + generic.ResourceOption Image string `json:"image"` } @@ -34,8 +35,8 @@ func NewUpgradeOptions() *UpgradeOptions { return &UpgradeOptions{} } -// GetUpgradeOperations creates upgrade opertaions -func GetUpgradeOperations(o *UpgradeOptions) *v1alpha1.OBClusterOperation { +// GetUpgradeOperation creates upgrade opertaions +func GetUpgradeOperation(o *UpgradeOptions) *v1alpha1.OBClusterOperation { upgradeOp := &v1alpha1.OBClusterOperation{ ObjectMeta: v1.ObjectMeta{ Name: o.Name + "-upgrade-" + rand.String(6), @@ -53,14 +54,13 @@ func GetUpgradeOperations(o *UpgradeOptions) *v1alpha1.OBClusterOperation { func (o *UpgradeOptions) Validate() error { if o.Image == "" { - return errors.New("image is required") + return errors.New("image is not specified") } return nil } // AddFlags for upgrade options func (o *UpgradeOptions) AddFlags(cmd *cobra.Command) { - // set image to null, avoid image downgrade - cmd.Flags().StringVar(&o.Namespace, "namespace", "default", "namespace of ob cluster") - cmd.Flags().StringVar(&o.Image, "image", "", "The image of observer") + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of ob cluster") + cmd.Flags().StringVar(&o.Image, FLAG_OBSERVER_IMAGE, "", "The image of observer") // set image to null, avoid image downgrade } diff --git a/internal/cli/cmd/cluster/cluster.go b/internal/cli/cmd/cluster/cluster.go index 4b1f73a40..9667aee29 100644 --- a/internal/cli/cmd/cluster/cluster.go +++ b/internal/cli/cmd/cluster/cluster.go @@ -20,9 +20,10 @@ import ( // NewCmd is command for cluster management func NewCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "cluster ", - Short: "Command for cluster management", - Long: `Command for cluster management, such as Create, UpGrade, Delete, Scale, Show.`, + Use: "cluster ", + Aliases: []string{"c"}, + Short: "Command for cluster management", + Long: `Command for cluster management, such as Create, UpGrade, Delete, Scale, Show.`, } cmd.AddCommand(NewCreateCmd()) cmd.AddCommand(NewDeleteCmd()) diff --git a/internal/cli/cmd/cluster/create.go b/internal/cli/cmd/cluster/create.go index dd29a7c72..d9b21cc7d 100644 --- a/internal/cli/cmd/cluster/create.go +++ b/internal/cli/cmd/cluster/create.go @@ -20,7 +20,6 @@ import ( cluster "github.com/oceanbase/ob-operator/internal/cli/cluster" cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" "github.com/oceanbase/ob-operator/internal/clients" - oberr "github.com/oceanbase/ob-operator/pkg/errors" ) // NewCreateCmd create an ob cluster @@ -46,7 +45,7 @@ func NewCreateCmd() *cobra.Command { } _, err := clients.CreateOBCluster(cmd.Context(), obcluster) if err != nil { - logger.Fatalln(oberr.NewInternal(err.Error())) + logger.Fatalln(err) } logger.Printf("Create obcluster instance: %s", o.ClusterName) logger.Printf("Run `echo $(kubectl get secret %s -o jsonpath='{.data.password}'|base64 --decode)` to get the secrets", obcluster.Spec.UserSecrets.Root) diff --git a/internal/cli/cmd/cluster/delete.go b/internal/cli/cmd/cluster/delete.go index 2ffb51f09..fcc0718a5 100644 --- a/internal/cli/cmd/cluster/delete.go +++ b/internal/cli/cmd/cluster/delete.go @@ -21,7 +21,7 @@ import ( "github.com/oceanbase/ob-operator/internal/clients" ) -// NewDeleteCmd delete ob clusters +// NewDeleteCmd delete ob cluster func NewDeleteCmd() *cobra.Command { o := cluster.NewDeleteOptions() logger := cmdUtil.GetDefaultLoggerInstance() @@ -36,7 +36,7 @@ func NewDeleteCmd() *cobra.Command { if err != nil { logger.Fatalln(err) } - logger.Printf("Delete ob cluster %s success", o.Name) + logger.Printf("Delete ob cluster %s successfully", o.Name) }, } o.AddFlags(cmd) diff --git a/internal/cli/cmd/cluster/list.go b/internal/cli/cmd/cluster/list.go index 1ff1bc0f3..c245716db 100644 --- a/internal/cli/cmd/cluster/list.go +++ b/internal/cli/cmd/cluster/list.go @@ -40,10 +40,10 @@ func NewListCmd() *cobra.Command { return obclusterList.Items[i].Name < obclusterList.Items[j].Name }) if len(obclusterList.Items) == 0 { - logger.Println("No clusters found") + logger.Println("No OBClusters found") return } - tbLog.Println("Namespace \t Name \t Create Time \t Status") + tbLog.Println("NAMESPACE \t NAME \t CREATE TIME \t STATUS") for _, cluster := range obclusterList.Items { tbLog.Printf("%s \t %s \t %s \t %s\n", cluster.Namespace, cluster.Name, cluster.CreationTimestamp, cluster.Status.Status) } diff --git a/internal/cli/cmd/cluster/scale.go b/internal/cli/cmd/cluster/scale.go index 7520e6fa1..0d320ee3a 100644 --- a/internal/cli/cmd/cluster/scale.go +++ b/internal/cli/cmd/cluster/scale.go @@ -14,15 +14,11 @@ See the Mulan PSL v2 for more details. package cluster import ( - "fmt" - "github.com/spf13/cobra" cluster "github.com/oceanbase/ob-operator/internal/cli/cluster" cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" "github.com/oceanbase/ob-operator/internal/clients" - clusterstatus "github.com/oceanbase/ob-operator/internal/const/status/obcluster" - oberr "github.com/oceanbase/ob-operator/pkg/errors" ) // NewScaleCmd scale zones in ob cluster @@ -32,29 +28,30 @@ func NewScaleCmd() *cobra.Command { cmd := &cobra.Command{ Use: "scale ", Args: cobra.ExactArgs(1), - Short: "scale ob cluster", + Short: "Scale ob cluster", + Long: `Scale ob cluster, support add/adjust/delete of zones.`, PreRunE: o.Parse, Run: func(cmd *cobra.Command, args []string) { obcluster, err := clients.GetOBCluster(cmd.Context(), o.Namespace, o.Name) if err != nil { logger.Fatalln(err) } - if obcluster.Status.Status != clusterstatus.Running { - logger.Fatalln(fmt.Errorf("Obcluster status invalid, Status:%s", obcluster.Status.Status)) + if err := cmdUtil.CheckClusterStatus(obcluster); err != nil { + logger.Fatalln(err) + } else { + o.OldTopology = obcluster.Spec.Topology } - o.OldTopology = obcluster.Spec.Topology if err := o.Validate(); err != nil { logger.Fatalln(err) } if err := o.Complete(); err != nil { logger.Fatalln(err) } - scaleOp := cluster.GetScaleOperation(o) - op, err := clients.CreateOBClusterOperation(cmd.Context(), scaleOp) - if err != nil { - logger.Fatalln(oberr.NewInternal(err.Error())) + op := cluster.GetScaleOperation(o) + if _, err = clients.CreateOBClusterOperation(cmd.Context(), op); err != nil { + logger.Fatalln(err) } - logger.Printf("Create scale operation for obcluster %s success", op.Spec.OBCluster) + logger.Printf("Create scale operation for obcluster %s successfully", op.Spec.OBCluster) }, } o.AddFlags(cmd) diff --git a/internal/cli/cmd/cluster/show.go b/internal/cli/cmd/cluster/show.go index a2e71d386..aba1bb963 100644 --- a/internal/cli/cmd/cluster/show.go +++ b/internal/cli/cmd/cluster/show.go @@ -30,7 +30,7 @@ func NewShowCmd() *cobra.Command { tbw, tbLog := cmdUtil.GetTableLoggerInstance() cmd := &cobra.Command{ Use: "show ", - Short: "show overview of ob cluster", + Short: "Show overview of ob cluster", Args: cobra.ExactArgs(1), PreRunE: o.Parse, Run: func(cmd *cobra.Command, args []string) { @@ -38,33 +38,35 @@ func NewShowCmd() *cobra.Command { if err != nil { logger.Fatalln(err) } - obclusterOperation, err := clients.GetOBClusterOperations(cmd.Context(), obcluster) + obclusterOperationList, err := clients.GetOBClusterOperations(cmd.Context(), obcluster) if err != nil { logger.Fatalln(err) } - tbLog.Println("Cluster ID \t Name \t Status \t Image") + tbLog.Println("ClUSTER ID \t NAME \t STATUS \t IMAGE") tbLog.Printf("%d \t %s \t %s \t %s \n\n", obcluster.Spec.ClusterId, obcluster.Spec.ClusterName, obcluster.Status.Status, obcluster.Status.Image) if len(obcluster.Status.OBZoneStatus) > 0 { - tbLog.Println("Zone \t Status") + tbLog.Println("ZONE \t STATUS") for _, zone := range obcluster.Status.OBZoneStatus { tbLog.Printf("%s \t %s \n\n", zone.Zone, zone.Status) } } if len(obcluster.Status.Parameters) > 0 { - tbLog.Println("Key \t Value") + tbLog.Println("KEY \t VALUE") for _, Parameter := range obcluster.Status.Parameters { tbLog.Printf("%s \t %s \n\n", Parameter.Name, Parameter.Value) } } - if len(obclusterOperation.Items) > 0 { - sort.Slice(obclusterOperation.Items, func(i, j int) bool { - return obclusterOperation.Items[i].Name < obclusterOperation.Items[j].Name + if len(obclusterOperationList.Items) > 0 { + sort.Slice(obclusterOperationList.Items, func(i, j int) bool { + return obclusterOperationList.Items[i].Name < obclusterOperationList.Items[j].Name }) - tbLog.Println("Operation Type \t TTLDays \t Status") - for _, op := range obclusterOperation.Items { - tbLog.Printf("%s \t %d \t %s \n", op.Spec.Type, op.Spec.TTLDays, op.Status.Status) + tbLog.Println("OPERATION TYPE \t TTLDAYS \t STATUS \t CREATETIME") + for _, op := range obclusterOperationList.Items { + tbLog.Printf("%s \t %d \t %s \t %s\n", op.Spec.Type, op.Spec.TTLDays, op.Status.Status, op.CreationTimestamp) } + } else { + logger.Printf("No OBClusterOperations found in %s", obcluster.Spec.ClusterName) } if err = tbw.Flush(); err != nil { logger.Fatalln(err) diff --git a/internal/cli/cmd/cluster/update.go b/internal/cli/cmd/cluster/update.go index ddeeff512..efb9b2cd7 100644 --- a/internal/cli/cmd/cluster/update.go +++ b/internal/cli/cmd/cluster/update.go @@ -14,14 +14,11 @@ See the Mulan PSL v2 for more details. package cluster import ( - "fmt" - "github.com/spf13/cobra" cluster "github.com/oceanbase/ob-operator/internal/cli/cluster" cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" "github.com/oceanbase/ob-operator/internal/clients" - clusterstatus "github.com/oceanbase/ob-operator/internal/const/status/obcluster" ) // NewUpdateCmd update obcluster @@ -33,6 +30,7 @@ func NewUpdateCmd() *cobra.Command { Short: "Update ob cluster", Long: "Update ob cluster, support cpu/memory/storage", Args: cobra.ExactArgs(1), + Aliases: []string{"ud"}, PreRunE: o.Parse, Run: func(cmd *cobra.Command, args []string) { if err := o.Validate(); err != nil { @@ -45,15 +43,14 @@ func NewUpdateCmd() *cobra.Command { if err != nil { logger.Fatalln(err) } - if obcluster.Status.Status != clusterstatus.Running { - logger.Fatalln(fmt.Errorf("Obcluster status invalid, Status:%s", obcluster.Status.Status)) + if err := cmdUtil.CheckClusterStatus(obcluster); err != nil { + logger.Fatalln(err) } - updateOp := cluster.GetUpdateOperations(o) - op, err := clients.CreateOBClusterOperation(cmd.Context(), updateOp) - if err != nil { + op := cluster.GetUpdateOperation(o) + if _, err = clients.CreateOBClusterOperation(cmd.Context(), op); err != nil { logger.Fatalln(err) } - logger.Printf("Create update operation for obcluster %s success", op.Spec.OBCluster) + logger.Printf("Create update operation for obcluster %s successfully", op.Spec.OBCluster) }, } o.AddFlags(cmd) diff --git a/internal/cli/cmd/cluster/upgrade.go b/internal/cli/cmd/cluster/upgrade.go index bce20b765..3e570d06b 100644 --- a/internal/cli/cmd/cluster/upgrade.go +++ b/internal/cli/cmd/cluster/upgrade.go @@ -14,14 +14,11 @@ See the Mulan PSL v2 for more details. package cluster import ( - "fmt" - "github.com/spf13/cobra" cluster "github.com/oceanbase/ob-operator/internal/cli/cluster" cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" "github.com/oceanbase/ob-operator/internal/clients" - clusterstatus "github.com/oceanbase/ob-operator/internal/const/status/obcluster" ) // NewUpgradeCmd upgrade obclusters @@ -33,6 +30,7 @@ func NewUpgradeCmd() *cobra.Command { Short: "Upgrade ob cluster", Long: "Upgrade ob cluster, please specify the new image", Args: cobra.ExactArgs(1), + Aliases: []string{"ug"}, PreRunE: o.Parse, Run: func(cmd *cobra.Command, args []string) { if err := o.Validate(); err != nil { @@ -42,15 +40,14 @@ func NewUpgradeCmd() *cobra.Command { if err != nil { logger.Fatalln(err) } - if obcluster.Status.Status != clusterstatus.Running { - logger.Fatalln(fmt.Errorf("Obcluster status invalid, Status:%s", obcluster.Status.Status)) + if err := cmdUtil.CheckClusterStatus(obcluster); err != nil { + logger.Fatalln(err) } - upgradeOp := cluster.GetUpgradeOperations(o) - op, err := clients.CreateOBClusterOperation(cmd.Context(), upgradeOp) - if err != nil { + op := cluster.GetUpgradeOperation(o) + if _, err = clients.CreateOBClusterOperation(cmd.Context(), op); err != nil { logger.Fatalln(err) } - logger.Printf("Create upgrade operation for obcluster %s success", op.Spec.OBCluster) + logger.Printf("Create upgrade operation for obcluster %s successfully", op.Spec.OBCluster) }, } o.AddFlags(cmd) diff --git a/internal/cli/cmd/install/install.go b/internal/cli/cmd/install/install.go index ad93d2911..8df241127 100644 --- a/internal/cli/cmd/install/install.go +++ b/internal/cli/cmd/install/install.go @@ -25,8 +25,9 @@ func NewCmd() *cobra.Command { o := install.NewInstallOptions() logger := cmdUtil.GetDefaultLoggerInstance() cmd := &cobra.Command{ - Use: "install ", - Short: "Command for ob-operator and components installation", + Use: "install ", + Aliases: []string{"i"}, + Short: "Command for ob-operator and components installation", Long: `Command for ob-operator and components installation. Currently support: diff --git a/internal/cli/cmd/tenant/activate.go b/internal/cli/cmd/tenant/activate.go new file mode 100644 index 000000000..bf8248320 --- /dev/null +++ b/internal/cli/cmd/tenant/activate.go @@ -0,0 +1,61 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" + "github.com/oceanbase/ob-operator/internal/clients" +) + +// NewActivateCmd activates a standby obtenant +func NewActivateCmd() *cobra.Command { + o := tenant.NewActivateOptions() + logger := cmdUtil.GetDefaultLoggerInstance() + cmd := &cobra.Command{ + Use: "activate ", + Short: "Activate a standby tenant", + PreRunE: o.Parse, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if err := o.Validate(); err != nil { + logger.Fatalln(err) + } + obtenant, err := clients.GetOBTenant(cmd.Context(), types.NamespacedName{ + Name: o.Name, + Namespace: o.Namespace, + }) + if err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckTenantStatus(obtenant); err != nil { + logger.Fatalln(err) + } + if obtenant.Status.TenantRole == apiconst.TenantRolePrimary { + logger.Fatalf("Obtenant %s is already PRIMARY", o.Name) + } + op := tenant.GetActivateOperation(o) + if _, err = clients.CreateOBTenantOperation(cmd.Context(), op); err != nil { + logger.Fatalln(err) + } + logger.Printf("Create activate operation for tenant %s successfully", o.Name) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/tenant/changepwd.go b/internal/cli/cmd/tenant/changepwd.go new file mode 100644 index 000000000..2bb05304d --- /dev/null +++ b/internal/cli/cmd/tenant/changepwd.go @@ -0,0 +1,62 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" + "github.com/oceanbase/ob-operator/internal/clients" +) + +// NewChangePwdCmd changes password of an obtenant +func NewChangePwdCmd() *cobra.Command { + o := tenant.NewChangePwdOptions() + logger := cmdUtil.GetDefaultLoggerInstance() + cmd := &cobra.Command{ + Use: "changepwd --password=", + Short: "Change password of an ob tenant", + PreRunE: o.Parse, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if err := o.Validate(); err != nil { + logger.Fatalln(err) + } + obtenant, err := clients.GetOBTenant(cmd.Context(), types.NamespacedName{ + Name: o.Name, + Namespace: o.Namespace, + }) + if err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckTenantStatus(obtenant); err != nil { + logger.Fatalln(err) + } + if err := tenant.GenerateNewPwd(cmd.Context(), o); err != nil { + logger.Fatalln(err) + } else { + logger.Println("New password generated success") + } + op := tenant.GetChangePwdOperation(o) + if _, err = clients.CreateOBTenantOperation(cmd.Context(), op); err != nil { + logger.Fatalln(err) + } + logger.Printf("Create changepwd operation for obtenant %s successfully", o.Name) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/tenant/create.go b/internal/cli/cmd/tenant/create.go new file mode 100644 index 000000000..eb11b4ec1 --- /dev/null +++ b/internal/cli/cmd/tenant/create.go @@ -0,0 +1,50 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" +) + +// NewCreateCmd create an ob tenant +func NewCreateCmd() *cobra.Command { + o := tenant.NewCreateOptions() + logger := cmdUtil.GetDefaultLoggerInstance() + cmd := &cobra.Command{ + Use: "create --cluster=", + Short: "Create ob tenant", + Aliases: []string{"c"}, + PreRunE: o.Parse, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if err := o.Complete(); err != nil { + logger.Fatalln(err) + } + if err := o.Validate(); err != nil { + logger.Fatalln(err) + } + obtenant, err := tenant.CreateOBTenant(cmd.Context(), o) + if err != nil { + logger.Fatalln(err) + } + logger.Printf("Create obtenant instance: %s", o.TenantName) + logger.Printf("Run `echo $(kubectl get secret %s -o jsonpath='{.data.password}'|base64 --decode)` to get the secrets", obtenant.Spec.Credentials.Root) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/tenant/delete.go b/internal/cli/cmd/tenant/delete.go new file mode 100644 index 000000000..0f3b53b61 --- /dev/null +++ b/internal/cli/cmd/tenant/delete.go @@ -0,0 +1,48 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" + "github.com/oceanbase/ob-operator/internal/clients" +) + +// NewDeleteCmd delete ob tenant +func NewDeleteCmd() *cobra.Command { + o := tenant.NewDeleteOptions() + logger := cmdUtil.GetDefaultLoggerInstance() + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete ob tenant", + Aliases: []string{"d"}, + Args: cobra.ExactArgs(1), + PreRunE: o.Parse, + Run: func(cmd *cobra.Command, args []string) { + err := clients.DeleteOBTenant(cmd.Context(), types.NamespacedName{ + Namespace: o.Namespace, + Name: o.Name, + }) + if err != nil { + logger.Fatalln(err) + } + logger.Printf("Delete ob tenant %s successfully", o.Name) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/tenant/list.go b/internal/cli/cmd/tenant/list.go new file mode 100644 index 000000000..e86dcf8db --- /dev/null +++ b/internal/cli/cmd/tenant/list.go @@ -0,0 +1,58 @@ +/* +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 tenant + +import ( + "sort" + + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" + "github.com/oceanbase/ob-operator/internal/clients" +) + +// NewListCmd list all ob tenants +func NewListCmd() *cobra.Command { + o := tenant.NewListOptions() + tbw, tbLog := cmdUtil.GetTableLoggerInstance() + logger := cmdUtil.GetDefaultLoggerInstance() + cmd := &cobra.Command{ + Use: "list", + Short: "List ob tenants", + Aliases: []string{"ls", "l"}, + Run: func(cmd *cobra.Command, args []string) { + obtenantList, err := clients.ListAllOBTenants(cmd.Context(), o.Namespace, v1.ListOptions{}) + if err != nil { + logger.Fatalln(err.Error()) + } + sort.Slice(obtenantList.Items, func(i, j int) bool { + return obtenantList.Items[i].Name < obtenantList.Items[j].Name + }) + if len(obtenantList.Items) == 0 { + logger.Println("No OBTenants found") + return + } + tbLog.Println("NAMESPACE \t NAME \t CLUSTERNAME \t TENANTNAME \t TENANTROLE \t CREATETIME \t STATUS") + for _, tenant := range obtenantList.Items { + tbLog.Printf("%s \t %s \t %s \t %s \t %s \t %s \t %s\n", tenant.Namespace, tenant.ObjectMeta.Name, tenant.Spec.ClusterName, tenant.Spec.TenantName, tenant.Status.TenantRole, tenant.CreationTimestamp, tenant.Status.Status) + } + if err := tbw.Flush(); err != nil { + logger.Fatalln(err) + } + }, + } + return cmd +} diff --git a/internal/cli/cmd/tenant/replaylog.go b/internal/cli/cmd/tenant/replaylog.go new file mode 100644 index 000000000..5ef8a1c23 --- /dev/null +++ b/internal/cli/cmd/tenant/replaylog.go @@ -0,0 +1,59 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" + "github.com/oceanbase/ob-operator/internal/clients" +) + +// NewReplayLogCmd replay log of an ob tenant +func NewReplayLogCmd() *cobra.Command { + o := tenant.NewReplayLogOptions() + logger := cmdUtil.GetDefaultLoggerInstance() + cmd := &cobra.Command{ + Use: "replaylog ", + Short: "replay log of an ob tenant", + Aliases: []string{"r", "rl"}, + Args: cobra.ExactArgs(1), + PreRunE: o.Parse, + Run: func(cmd *cobra.Command, args []string) { + obtenant, err := clients.GetOBTenant(cmd.Context(), types.NamespacedName{ + Namespace: o.Namespace, + Name: o.Name, + }) + if err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckTenantStatus(obtenant); err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckTenantRole(obtenant, apiconst.TenantRoleStandby); err != nil { + logger.Fatalln(err) + } + op := tenant.GetReplayLogOperation(o) + if _, err = clients.CreateOBTenantOperation(cmd.Context(), op); err != nil { + logger.Fatalln(err) + } + logger.Printf("Create replay log operation of tenant %s successfully", o.Name) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/tenant/scale.go b/internal/cli/cmd/tenant/scale.go new file mode 100644 index 000000000..95a36bb83 --- /dev/null +++ b/internal/cli/cmd/tenant/scale.go @@ -0,0 +1,63 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" + "github.com/oceanbase/ob-operator/internal/clients" +) + +// NewScaleCmd scale an obtenant +func NewScaleCmd() *cobra.Command { + o := tenant.NewScaleOptions() + logger := cmdUtil.GetDefaultLoggerInstance() + cmd := &cobra.Command{ + Use: "scale ", + Short: "Scale ob tenant", + Long: `Scale ob tenant, support unit-number/unit config of zones.`, + Args: cobra.ExactArgs(1), + PreRunE: o.Parse, + Run: func(cmd *cobra.Command, args []string) { + obtenant, err := clients.GetOBTenant(cmd.Context(), types.NamespacedName{ + Name: o.Name, + Namespace: o.Namespace, + }) + if err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckTenantStatus(obtenant); err != nil { + logger.Fatalln(err) + } else { + o.OldResourcePools = obtenant.Spec.Pools + } + if err := o.Validate(); err != nil { + logger.Fatalln(err) + } + if err := o.Complete(); err != nil { + logger.Fatalln(err) + } + op := tenant.GetScaleOperation(o) + if _, err = clients.CreateOBTenantOperation(cmd.Context(), op); err != nil { + logger.Fatalln(err) + } + logger.Printf("Create scale operation for obtenant %s successfully", o.Name) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/tenant/show.go b/internal/cli/cmd/tenant/show.go new file mode 100644 index 000000000..182954d54 --- /dev/null +++ b/internal/cli/cmd/tenant/show.go @@ -0,0 +1,75 @@ +/* +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 tenant + +import ( + "sort" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" + "github.com/oceanbase/ob-operator/internal/clients" +) + +// NewShowCmd show the overview and operations of ob tenant +func NewShowCmd() *cobra.Command { + o := tenant.NewShowOptions() + logger := cmdUtil.GetDefaultLoggerInstance() + tbw, tbLog := cmdUtil.GetTableLoggerInstance() + cmd := &cobra.Command{ + Use: "show ", + Short: "Show overview of an ob tenant", + Args: cobra.ExactArgs(1), + PreRunE: o.Parse, + Run: func(cmd *cobra.Command, args []string) { + obtenant, err := clients.GetOBTenant(cmd.Context(), types.NamespacedName{ + Namespace: o.Namespace, + Name: o.Name, + }) + if err != nil { + logger.Fatalln(err) + } + obtenantOperationList, err := clients.GetOBTenantOperations(cmd.Context(), obtenant) + if err != nil { + logger.Fatalln(err) + } + tbLog.Println("TENANTNAME \t CLUSTERNAME \t TENANTROLE \t STATUS") + tbLog.Printf("%s \t %s \t %s \t %s \n\n", obtenant.Spec.TenantName, obtenant.Spec.ClusterName, obtenant.Status.TenantRole, obtenant.Status.Status) + if len(obtenant.Status.Pools) > 0 { + tbLog.Println("ZONELIST \t UNITNUM \t PRIORITY") + for _, pool := range obtenant.Status.Pools { + tbLog.Printf("%s \t %d \t %d\n\n", pool.ZoneList, pool.UnitNumber, pool.Priority) + } + } + if len(obtenantOperationList.Items) > 0 { + sort.Slice(obtenantOperationList.Items, func(i, j int) bool { + return obtenantOperationList.Items[i].Name < obtenantOperationList.Items[j].Name + }) + tbLog.Println("OPERATION TYPE \t STATUS \t CREATETIME") + for _, op := range obtenantOperationList.Items { + tbLog.Printf("%s \t %s \t %s\n", op.Spec.Type, op.Status.Status, op.CreationTimestamp) + } + } else { + logger.Printf("No OBTenantOperations found in %s", obtenant.Spec.TenantName) + } + if err = tbw.Flush(); err != nil { + logger.Fatalln(err) + } + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/tenant/switchover.go b/internal/cli/cmd/tenant/switchover.go new file mode 100644 index 000000000..81658305a --- /dev/null +++ b/internal/cli/cmd/tenant/switchover.go @@ -0,0 +1,74 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" + "github.com/oceanbase/ob-operator/internal/clients" +) + +// NewSwitchOverCmd switchover two tenants +func NewSwitchOverCmd() *cobra.Command { + o := tenant.NewSwitchOverOptions() + logger := cmdUtil.GetDefaultLoggerInstance() + cmd := &cobra.Command{ + Use: "switchover ", + Short: "Switchover of primary tenant and standby tenant", + PreRunE: o.Parse, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + if err := o.Validate(); err != nil { + logger.Fatalln(err) + } + standbyTenant, err := clients.GetOBTenant(cmd.Context(), types.NamespacedName{ + Name: o.StandbyTenant, + Namespace: o.Namespace, + }) + if err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckTenantStatus(standbyTenant); err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckPrimaryTenant(standbyTenant); err != nil { + logger.Fatalln(err) + } + primaryTenant, err := clients.GetOBTenant(cmd.Context(), types.NamespacedName{ + Name: o.PrimaryTenant, + Namespace: o.Namespace, + }) + if err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckTenantStatus(primaryTenant); err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckTenantRole(primaryTenant, apiconst.TenantRolePrimary); err != nil { + logger.Fatalln(err) + } + op := tenant.GetSwitchOverOperation(o) + if _, err := clients.CreateOBTenantOperation(cmd.Context(), op); err != nil { + logger.Fatalln(err) + } + logger.Printf("Create switchover operation for primary tenant %s and standby tenant %s successfully", o.PrimaryTenant, o.StandbyTenant) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/tenant/tenant.go b/internal/cli/cmd/tenant/tenant.go index 16d4c1431..f4e2a8a57 100644 --- a/internal/cli/cmd/tenant/tenant.go +++ b/internal/cli/cmd/tenant/tenant.go @@ -14,21 +14,27 @@ See the Mulan PSL v2 for more details. package tenant import ( - "log" - "github.com/spf13/cobra" ) // NewCmd is command for tenant management func NewCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "tenant", - Short: "Command for tenant management", - Long: `Command for tenant management, such as Create, Update, Delete.`, - Run: func(cmd *cobra.Command, args []string) { - // TODO: not implemented - log.Println("oceanbase tenant management") - }, + Use: "tenant", + Aliases: []string{"t"}, + Short: "Command for tenant management", + Long: `Command for tenant management, such as Create, Update, Delete, Switchover, Activate, Replaylog.`, } + cmd.AddCommand(NewCreateCmd()) + cmd.AddCommand(NewDeleteCmd()) + cmd.AddCommand(NewUpdateCmd()) + cmd.AddCommand(NewScaleCmd()) + cmd.AddCommand(NewListCmd()) + cmd.AddCommand(NewUpgradeCmd()) + cmd.AddCommand(NewChangePwdCmd()) + cmd.AddCommand(NewSwitchOverCmd()) + cmd.AddCommand(NewActivateCmd()) + cmd.AddCommand(NewShowCmd()) + cmd.AddCommand(NewReplayLogCmd()) return cmd } diff --git a/internal/cli/cmd/tenant/update.go b/internal/cli/cmd/tenant/update.go new file mode 100644 index 000000000..ef14efc9b --- /dev/null +++ b/internal/cli/cmd/tenant/update.go @@ -0,0 +1,64 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" + "github.com/oceanbase/ob-operator/internal/clients" +) + +// NewUpdateCmd update an obtenant +func NewUpdateCmd() *cobra.Command { + o := tenant.NewUpdateOptions() + logger := cmdUtil.GetDefaultLoggerInstance() + cmd := &cobra.Command{ + Use: "update ", + Short: "Update ob tenant", + Long: "Update ob tenant, support unitNumber/connectWhiteList/priority of zones", + Args: cobra.ExactArgs(1), + Aliases: []string{"ud"}, + PreRunE: o.Parse, + Run: func(cmd *cobra.Command, args []string) { + obtenant, err := clients.GetOBTenant(cmd.Context(), types.NamespacedName{ + Name: o.Name, + Namespace: o.Namespace, + }) + if err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckTenantStatus(obtenant); err != nil { + logger.Fatalln(err) + } else { + o.OldResourcePools = obtenant.Spec.Pools + } + if err := o.Validate(); err != nil { + logger.Fatalln(err) + } + if err := o.Complete(); err != nil { + logger.Fatalln(err) + } + op := tenant.GetUpdateOperation(o) + if _, err = clients.CreateOBTenantOperation(cmd.Context(), op); err != nil { + logger.Fatalln(err) + } + logger.Printf("Create update operation for obtenant %s successfully", o.Name) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/tenant/upgrade.go b/internal/cli/cmd/tenant/upgrade.go new file mode 100644 index 000000000..3f8a25f22 --- /dev/null +++ b/internal/cli/cmd/tenant/upgrade.go @@ -0,0 +1,59 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + cmdUtil "github.com/oceanbase/ob-operator/internal/cli/cmd/util" + "github.com/oceanbase/ob-operator/internal/cli/tenant" + "github.com/oceanbase/ob-operator/internal/clients" +) + +// NewUpgradeCmd upgrade obtenant +func NewUpgradeCmd() *cobra.Command { + o := tenant.NewUpgradeOptions() + logger := cmdUtil.GetDefaultLoggerInstance() + cmd := &cobra.Command{ + Use: "upgrade ", + Short: "Upgrade ob tenant to compatible version to the cluster", + Long: `Upgrade ob tenant to higher version, suitable for restoring low-version backup data to a high-version cluster.`, + Args: cobra.ExactArgs(1), + Aliases: []string{"ug"}, + PreRunE: o.Parse, + Run: func(cmd *cobra.Command, args []string) { + if err := o.Validate(); err != nil { + logger.Fatalln(err) + } + obtenant, err := clients.GetOBTenant(cmd.Context(), types.NamespacedName{ + Name: o.Name, + Namespace: o.Namespace, + }) + if err != nil { + logger.Fatalln(err) + } + if err := cmdUtil.CheckTenantStatus(obtenant); err != nil { + logger.Fatalln(err) + } + op := tenant.GetUpgradeOperation(o) + if _, err = clients.CreateOBTenantOperation(cmd.Context(), op); err != nil { + logger.Fatalln(err) + } + logger.Printf("Create upgrade operation for obtenant %s successfully", o.Name) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/update/update.go b/internal/cli/cmd/update/update.go index 418496cef..3933c6cd6 100644 --- a/internal/cli/cmd/update/update.go +++ b/internal/cli/cmd/update/update.go @@ -25,8 +25,9 @@ func NewCmd() *cobra.Command { o := update.NewUpdateOptions() logger := cmdUtil.GetDefaultLoggerInstance() cmd := &cobra.Command{ - Use: "update ", - Short: "Command for ob-operator and components update", + Use: "update ", + Aliases: []string{"u"}, + Short: "Command for ob-operator and components update", Long: `Command for ob-operator and components update. Currently support: diff --git a/internal/cli/cmd/util/helper.go b/internal/cli/cmd/util/helper.go index 636c031f0..50aabcd67 100644 --- a/internal/cli/cmd/util/helper.go +++ b/internal/cli/cmd/util/helper.go @@ -20,17 +20,15 @@ import ( "github.com/spf13/pflag" ) +// PrintFlagValues is used for debugging func PrintFlagValues(cmd *cobra.Command) { - // 确保命令的 flag 已经被解析 _ = cmd.ParseFlags(nil) - // 遍历所有 flag flags := cmd.NonInheritedFlags() flags.VisitAll(func(f *pflag.Flag) { fmt.Printf("%s : %v\n", f.Name, f.Value.String()) }) - // 遍历继承的 flag(如果有) inheritedFlags := cmd.InheritedFlags() inheritedFlags.VisitAll(func(f *pflag.Flag) { fmt.Printf("%s : %v\n", f.Name, f.Value.String()) diff --git a/internal/cli/cmd/util/validator.go b/internal/cli/cmd/util/validator.go new file mode 100644 index 000000000..13c7ea9c7 --- /dev/null +++ b/internal/cli/cmd/util/validator.go @@ -0,0 +1,55 @@ +/* +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 util + +import ( + "fmt" + + apitypes "github.com/oceanbase/ob-operator/api/types" + "github.com/oceanbase/ob-operator/api/v1alpha1" + clusterstatus "github.com/oceanbase/ob-operator/internal/const/status/obcluster" + "github.com/oceanbase/ob-operator/internal/const/status/tenantstatus" +) + +// CheckTenantStatus checks running status of obtenant +func CheckTenantStatus(tenant *v1alpha1.OBTenant) error { + if tenant.Status.Status != tenantstatus.Running { + return fmt.Errorf("Obtenant status invalid, Status:%s", tenant.Status.Status) + } + return nil +} + +// CheckClusterStatus checks running status of obcluster +func CheckClusterStatus(cluster *v1alpha1.OBCluster) error { + if cluster.Status.Status != clusterstatus.Running { + return fmt.Errorf("Obcluster status invalid, Status:%s", cluster.Status.Status) + } + return nil +} + +// CheckPrimaryTenant checks primary tenant for a standbytenant +func CheckPrimaryTenant(standbytenant *v1alpha1.OBTenant) error { + if standbytenant.Spec.Source == nil || standbytenant.Spec.Source.Tenant == nil { + return fmt.Errorf("Obtenant %s has no primary tenant", standbytenant.Name) + } + return nil +} + +// CheckTenantRole checks tenant role +func CheckTenantRole(tenant *v1alpha1.OBTenant, role apitypes.TenantRole) error { + if tenant.Status.TenantRole != role { + return fmt.Errorf("The tenant is not %s tenant", string(role)) + } + return nil +} diff --git a/internal/cli/generated/bindata/bindata.go b/internal/cli/generated/bindata/bindata.go index 39f8df169..a3fd9db6e 100644 --- a/internal/cli/generated/bindata/bindata.go +++ b/internal/cli/generated/bindata/bindata.go @@ -92,7 +92,7 @@ func internalAssetsCliTemplatesComponent_configYaml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/cli-templates/component_config.yaml", size: 219, mode: os.FileMode(436), modTime: time.Unix(1725375864, 0)} + info := bindataFileInfo{name: "internal/assets/cli-templates/component_config.yaml", size: 219, mode: os.FileMode(420), modTime: time.Unix(1727333933, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -156,11 +156,13 @@ var _bindata = map[string]func() (*asset, error){ // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png +// +// data/ +// foo.txt +// img/ +// a.png +// b.png +// // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error diff --git a/internal/cli/cluster/generic.go b/internal/cli/generic/generic.go similarity index 52% rename from internal/cli/cluster/generic.go rename to internal/cli/generic/generic.go index 2443bfd32..6eb96ee37 100644 --- a/internal/cli/cluster/generic.go +++ b/internal/cli/generic/generic.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 cluster +package generic import ( "errors" @@ -19,31 +19,38 @@ import ( "github.com/spf13/cobra" ) -type ResourceOptions struct { +type ResourceOption struct { Name string Namespace string + Cmd *cobra.Command } // Parse the args in obocli -func (o *ResourceOptions) Parse(_ *cobra.Command, args []string) error { +func (o *ResourceOption) Parse(cmd *cobra.Command, args []string) error { o.Name = args[0] + o.Cmd = cmd return nil } -// Complete the unset params in options -func (o *ResourceOptions) Complete() error { +// Complete the unset params in option +func (o *ResourceOption) Complete() error { return nil } -// Validate the params in options -func (o *ResourceOptions) Validate() error { +// Validate the params in option +func (o *ResourceOption) Validate() error { if o.Namespace == "" { - return errors.New("namespace not specified") + return errors.New("namespace is not specified") } return nil } -// AddFlags add basic flags for cluster management -func (o *ResourceOptions) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVar(&o.Namespace, "namespace", "default", "namespace of ob cluster") +// CheckIfFlagChanged checks if flags has changed +func (o *ResourceOption) CheckIfFlagChanged(flags ...string) bool { + for _, flagName := range flags { + if flag := o.Cmd.Flags().Lookup(flagName); flag != nil && flag.Changed { + return true + } + } + return false } diff --git a/internal/cli/tenant/activate.go b/internal/cli/tenant/activate.go new file mode 100644 index 000000000..85ada0937 --- /dev/null +++ b/internal/cli/tenant/activate.go @@ -0,0 +1,58 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" + oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" +) + +type ActivateOptions struct { + generic.ResourceOption + force bool +} + +func NewActivateOptions() *ActivateOptions { + return &ActivateOptions{} +} + +func GetActivateOperation(o *ActivateOptions) *v1alpha1.OBTenantOperation { + activateOp := &v1alpha1.OBTenantOperation{ + ObjectMeta: v1.ObjectMeta{ + Name: o.Name + "-activate-" + rand.String(6), + Namespace: o.Namespace, + Labels: map[string]string{oceanbaseconst.LabelRefOBTenantOp: o.Name}, + }, + Spec: v1alpha1.OBTenantOperationSpec{ + Type: apiconst.TenantOpFailover, + Failover: &v1alpha1.OBTenantOpFailoverSpec{ + StandbyTenant: o.Name, + }, + Force: o.force, + }, + } + return activateOp +} + +// AddFlags add basic flags for tenant management +func (o *ActivateOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of ob tenant") + cmd.Flags().BoolVarP(&o.force, FLAG_FORCE, "f", false, "force operation") +} diff --git a/internal/cli/tenant/changepwd.go b/internal/cli/tenant/changepwd.go new file mode 100644 index 000000000..4a665a2b0 --- /dev/null +++ b/internal/cli/tenant/changepwd.go @@ -0,0 +1,93 @@ +/* +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 tenant + +import ( + "context" + "errors" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" + oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" + oberr "github.com/oceanbase/ob-operator/pkg/errors" + "github.com/oceanbase/ob-operator/pkg/k8s/client" +) + +type ChangePwdOptions struct { + generic.ResourceOption + Password string `json:"password" binding:"required"` + RootSecretName string `json:"rootSecretName" binding:"required"` + force bool +} + +func NewChangePwdOptions() *ChangePwdOptions { + return &ChangePwdOptions{} +} + +func GetChangePwdOperation(o *ChangePwdOptions) *v1alpha1.OBTenantOperation { + changePwdOp := &v1alpha1.OBTenantOperation{ + ObjectMeta: v1.ObjectMeta{ + Name: o.Name + "-change-root-pwd-" + rand.String(6), + Namespace: o.Namespace, + Labels: map[string]string{oceanbaseconst.LabelRefOBTenantOp: o.Name}, + }, + Spec: v1alpha1.OBTenantOperationSpec{ + Type: apiconst.TenantOpChangePwd, + ChangePwd: &v1alpha1.OBTenantOpChangePwdSpec{ + Tenant: o.Name, + SecretRef: o.RootSecretName, + }, + Force: o.force, + }, + } + return changePwdOp +} + +// GenerateNewPwd generate new password for obtenant +func GenerateNewPwd(ctx context.Context, o *ChangePwdOptions) error { + k8sclient := client.GetClient() + o.RootSecretName = o.Name + "-root-" + rand.String(6) + _, err := k8sclient.ClientSet.CoreV1().Secrets(o.Namespace).Create(ctx, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: o.RootSecretName, + Namespace: o.Namespace, + }, + StringData: map[string]string{ + "password": o.Password, + }, + }, v1.CreateOptions{}) + if err != nil { + return oberr.NewInternal(err.Error()) + } + return nil +} + +func (o *ChangePwdOptions) Validate() error { + if o.Password == "" { + return errors.New("Password can not be empty") + } + return nil +} + +func (o *ChangePwdOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of ob tenant") + cmd.Flags().StringVarP(&o.Password, FLAG_PASSWD, "p", "", "new password of ob tenant") + cmd.Flags().BoolVarP(&o.force, FLAG_FORCE, "f", false, "force operation") +} diff --git a/internal/cli/tenant/create.go b/internal/cli/tenant/create.go new file mode 100644 index 000000000..4d05cf228 --- /dev/null +++ b/internal/cli/tenant/create.go @@ -0,0 +1,456 @@ +/* +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 tenant + +import ( + "context" + "errors" + "fmt" + "math" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + corev1 "k8s.io/api/core/v1" + kubeerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/rand" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + apitypes "github.com/oceanbase/ob-operator/api/types" + "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" + "github.com/oceanbase/ob-operator/internal/cli/utils" + "github.com/oceanbase/ob-operator/internal/clients" + "github.com/oceanbase/ob-operator/internal/clients/schema" + param "github.com/oceanbase/ob-operator/internal/dashboard/model/param" + oberr "github.com/oceanbase/ob-operator/pkg/errors" + "github.com/oceanbase/ob-operator/pkg/k8s/client" +) + +func NewCreateOptions() *CreateOptions { + return &CreateOptions{ + UnitConfig: ¶m.UnitConfig{}, + Pools: make([]param.ResourcePoolSpec, 0), + Source: ¶m.TenantSourceSpec{ + Restore: ¶m.RestoreSourceSpec{ + Until: ¶m.RestoreUntilConfig{}, + }, + }, + ZonePriority: make(map[string]string), + } +} + +type CreateOptions struct { + generic.ResourceOption + ClusterName string `json:"obcluster" binding:"required"` + TenantName string `json:"tenantName" binding:"required"` + UnitNumber int `json:"unitNum" binding:"required"` + RootPassword string `json:"rootPassword" binding:"required"` + ConnectWhiteList string `json:"connectWhiteList,omitempty"` + Charset string `json:"charset,omitempty"` + + UnitConfig *param.UnitConfig `json:"unitConfig" binding:"required"` + Pools []param.ResourcePoolSpec `json:"pools" binding:"required"` + + // Enum: Primary, Standby + TenantRole string `json:"tenantRole,omitempty"` + Source *param.TenantSourceSpec `json:"source,omitempty"` + + // Flags for cli + From string `json:"from,omitempty"` + ZonePriority map[string]string `json:"zones"` + Restore bool `json:"restore"` + RestoreType string `json:"restoreType"` + Timestamp string `json:"timestamp"` +} + +func (o *CreateOptions) Parse(_ *cobra.Command, args []string) error { + pools, err := utils.MapZonesToPools(o.ZonePriority) + if err != nil { + return err + } + o.Pools = pools + o.Name = args[0] + if o.CheckIfFlagChanged("from") { + o.Source.Tenant = &o.From + o.TenantRole = string(apiconst.TenantRolePrimary) + } else { + o.TenantRole = string(apiconst.TenantRoleStandby) + } + // create empty standby tenant + if !o.Restore { + o.Source.Restore = nil + } + return nil +} + +func (o *CreateOptions) Complete() error { + if o.RootPassword == "" { + o.RootPassword = utils.GenerateRandomPassword(8, 32) + } + if o.Timestamp != "" { + o.Source.Restore.Until.Timestamp = &o.Timestamp + } + return nil +} + +func (o *CreateOptions) Validate() error { + if o.Namespace == "" { + return errors.New("namespace is not specified") + } + if o.ClusterName == "" { + return errors.New("cluster name is not specified") + } + if o.TenantName == "" { + return errors.New("tenant name is not specified") + } + if !utils.CheckResourceName(o.Name) { + return fmt.Errorf("invalid resource name in k8s: %s", o.Name) + } + if !utils.CheckTenantName(o.TenantName) { + return fmt.Errorf("invalid tenant name: %s, the first letter must be a letter or an underscore and cannot contain -", o.TenantName) + } + if o.Source != nil && o.Source.Tenant != nil && o.TenantRole == "PRIMARY" { + return fmt.Errorf("invalid tenant role") + } + if o.Restore && o.RestoreType != "OSS" && o.RestoreType != "NFS" { + return errors.New("Restore Type not supported") + } + if o.Restore && o.RestoreType == "OSS" && o.Source.Restore.OSSAccessKey == "" { + return errors.New("oss access key not specified") + } + if o.Restore && o.RestoreType == "NFS" && o.Source.Restore.BakEncryptionPassword == "" { + return errors.New("back encryption password not specified") + } + return nil +} + +// CreateOBTenant create an obtenant with configs +func CreateOBTenant(ctx context.Context, p *CreateOptions) (*v1alpha1.OBTenant, error) { + nn := types.NamespacedName{ + Namespace: p.Namespace, + Name: p.Name, + } + t, err := buildOBTenantApiType(nn, p) + if err != nil { + return nil, err + } + if p.RootPassword != "" { + t.Spec.Credentials.Root = p.Name + "-root-" + rand.String(6) + } + + k8sclient := client.GetClient() + + if p.Source != nil && p.Source.Tenant != nil { + // Check primary tenant + ns := nn.Namespace + tenantCR := *p.Source.Tenant + if strings.Contains(*p.Source.Tenant, "/") { + splits := strings.Split(*p.Source.Tenant, "/") + if len(splits) != 2 { + return nil, oberr.NewBadRequest("invalid tenant name") + } + ns, tenantCR = splits[0], splits[1] + } + existing, err := clients.GetOBTenant(ctx, types.NamespacedName{ + Namespace: ns, + Name: tenantCR, + }) + if err != nil { + if kubeerrors.IsNotFound(err) { + return nil, oberr.NewBadRequest("primary tenant not found") + } + return nil, oberr.NewInternal(err.Error()) + } + if existing.Status.TenantRole != apiconst.TenantRolePrimary { + return nil, oberr.NewBadRequest("the target tenant is not primary tenant") + } + // Match root password + rootSecret, err := k8sclient.ClientSet.CoreV1().Secrets(existing.Namespace).Get(ctx, existing.Status.Credentials.Root, v1.GetOptions{}) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } + if pwd, ok := rootSecret.Data["password"]; ok { + if p.RootPassword != string(pwd) { + return nil, oberr.NewBadRequest("root password not match") + } + if t.Spec.Credentials.Root != "" { + err = createPasswordSecret(ctx, types.NamespacedName{ + Namespace: nn.Namespace, + Name: t.Spec.Credentials.Root, + }, p.RootPassword) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } + } + } + + // Fetch standbyro password + standbyroSecret, err := k8sclient.ClientSet.CoreV1().Secrets(existing.Namespace).Get(ctx, existing.Status.Credentials.StandbyRO, v1.GetOptions{}) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } + + if pwd, ok := standbyroSecret.Data["password"]; ok { + t.Spec.Credentials.StandbyRO = p.Name + "-standbyro-" + rand.String(6) + err = createPasswordSecret(ctx, types.NamespacedName{ + Namespace: nn.Namespace, + Name: t.Spec.Credentials.StandbyRO, + }, string(pwd)) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } + } + } else { + if t.Spec.Credentials.Root != "" { + err = createPasswordSecret(ctx, types.NamespacedName{ + Namespace: nn.Namespace, + Name: t.Spec.Credentials.Root, + }, p.RootPassword) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } + } + t.Spec.Credentials.StandbyRO = p.Name + "-standbyro-" + rand.String(6) + err = createPasswordSecret(ctx, types.NamespacedName{ + Namespace: nn.Namespace, + Name: t.Spec.Credentials.StandbyRO, + }, rand.String(32)) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } + } + // Restore + if p.Source != nil && p.Source.Restore != nil { + if p.Source.Restore.BakEncryptionPassword != "" { + secretName := p.Name + "-bak-encryption-" + rand.String(6) + t.Spec.Source.Restore.BakEncryptionSecret = secretName + _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(ctx, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: secretName, + Namespace: nn.Namespace, + }, + StringData: map[string]string{ + "password": p.Source.Restore.BakEncryptionPassword, + }, + }, v1.CreateOptions{}) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } + } + + if p.Source.Restore.OSSAccessID != "" && p.Source.Restore.OSSAccessKey != "" { + ossSecretName := p.Name + "-oss-access-" + rand.String(6) + t.Spec.Source.Restore.ArchiveSource.OSSAccessSecret = ossSecretName + t.Spec.Source.Restore.BakDataSource.OSSAccessSecret = ossSecretName + _, err = k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(ctx, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: ossSecretName, + Namespace: nn.Namespace, + }, + StringData: map[string]string{ + "accessId": p.Source.Restore.OSSAccessID, + "accessKey": p.Source.Restore.OSSAccessKey, + }, + }, v1.CreateOptions{}) + if err != nil { + return nil, oberr.NewInternal(err.Error()) + } + } + } + + tenant, err := clients.CreateOBTenant(ctx, t) + if err != nil { + return nil, err + } + return tenant, nil +} + +func buildOBTenantApiType(nn types.NamespacedName, p *CreateOptions) (*v1alpha1.OBTenant, error) { + t := &v1alpha1.OBTenant{ + ObjectMeta: v1.ObjectMeta{ + Name: nn.Name, + Namespace: nn.Namespace, + }, + TypeMeta: v1.TypeMeta{ + Kind: schema.OBTenantKind, + APIVersion: schema.OBTenantGroup + "/" + schema.OBTenantVersion, + }, + Spec: v1alpha1.OBTenantSpec{ + ClusterName: p.ClusterName, + TenantName: p.TenantName, + UnitNumber: p.UnitNumber, + Charset: p.Charset, + ConnectWhiteList: p.ConnectWhiteList, + TenantRole: apitypes.TenantRole(p.TenantRole), + + // guard non-nil + Pools: []v1alpha1.ResourcePoolSpec{}, + }, + } + + if len(p.Pools) == 0 { + return nil, oberr.NewBadRequest("pools is empty") + } + if p.UnitConfig == nil { + return nil, oberr.NewBadRequest("unit config is nil") + } + + cpuCount, err := resource.ParseQuantity(p.UnitConfig.CPUCount) + if err != nil { + return nil, oberr.NewBadRequest("invalid cpu count: " + err.Error()) + } + memorySize, err := resource.ParseQuantity(p.UnitConfig.MemorySize) + if err != nil { + return nil, oberr.NewBadRequest("invalid memory size: " + err.Error()) + } + logDiskSize, err := resource.ParseQuantity(p.UnitConfig.LogDiskSize) + if err != nil { + return nil, oberr.NewBadRequest("invalid log disk size: " + err.Error()) + } + var maxIops, minIops int + if p.UnitConfig.MaxIops > math.MaxInt32 { + maxIops = math.MaxInt32 + } else { + maxIops = int(p.UnitConfig.MaxIops) + } + if p.UnitConfig.MinIops > math.MaxInt32 { + minIops = math.MaxInt32 + } else { + minIops = int(p.UnitConfig.MinIops) + } + + t.Spec.Pools = make([]v1alpha1.ResourcePoolSpec, 0, len(p.Pools)) + for i := range p.Pools { + apiPool := v1alpha1.ResourcePoolSpec{ + Zone: p.Pools[i].Zone, + Priority: p.Pools[i].Priority, + Type: &v1alpha1.LocalityType{}, + UnitConfig: &v1alpha1.UnitConfig{}, + } + apiPool.Type = &v1alpha1.LocalityType{ + Name: p.Pools[i].Type, + Replica: 1, + IsActive: true, + } + apiPool.UnitConfig = &v1alpha1.UnitConfig{ + MaxCPU: cpuCount, + MemorySize: memorySize, + MinCPU: cpuCount, + LogDiskSize: logDiskSize, + MaxIops: maxIops, + MinIops: minIops, + IopsWeight: p.UnitConfig.IopsWeight, + } + t.Spec.Pools = append(t.Spec.Pools, apiPool) + } + + if p.Source != nil { + t.Spec.Source = &v1alpha1.TenantSourceSpec{ + Tenant: p.Source.Tenant, + } + if p.Source.Restore != nil { + t.Spec.Source.Restore = &v1alpha1.RestoreSourceSpec{ + ArchiveSource: &apitypes.BackupDestination{}, + BakDataSource: &apitypes.BackupDestination{}, + // BakEncryptionSecret: p.Source.Restore.BakEncryptionSecret, + Until: v1alpha1.RestoreUntilConfig{}, + } + + t.Spec.Source.Restore.ArchiveSource.Type = apitypes.BackupDestType(p.RestoreType) + t.Spec.Source.Restore.ArchiveSource.Path = p.Source.Restore.ArchiveSource + t.Spec.Source.Restore.BakDataSource.Type = apitypes.BackupDestType(p.RestoreType) + t.Spec.Source.Restore.BakDataSource.Path = p.Source.Restore.BakDataSource + + if p.Source.Restore.Until != nil && !p.Source.Restore.Until.Unlimited { + t.Spec.Source.Restore.Until.Timestamp = p.Source.Restore.Until.Timestamp + } else { + t.Spec.Source.Restore.Until.Unlimited = true + } + } + } + return t, nil +} + +func createPasswordSecret(ctx context.Context, nn types.NamespacedName, password string) error { + k8sclient := client.GetClient() + _, err := k8sclient.ClientSet.CoreV1().Secrets(nn.Namespace).Create(ctx, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: nn.Name, + Namespace: nn.Namespace, + }, + StringData: map[string]string{ + "password": password, + }, + }, v1.CreateOptions{}) + return err +} + +// AddFlags for create options +func (o *CreateOptions) AddFlags(cmd *cobra.Command) { + o.AddBaseFlags(cmd) + o.AddUnitFlags(cmd) + o.AddPoolFlags(cmd) + o.AddRestoreFlags(cmd) +} + +// AddBaseFlags add base flags +func (o *CreateOptions) AddBaseFlags(cmd *cobra.Command) { + baseFlags := cmd.Flags() + baseFlags.StringVarP(&o.TenantName, FLAG_TENANT_NAME, "n", "", "Tenant name, if not specified, use name in k8s instead") + baseFlags.StringVar(&o.ClusterName, FLAG_CLUSTER_NAME, "", "The cluster name tenant belonged to in k8s") + baseFlags.StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "The namespace of the tenant") + baseFlags.StringVarP(&o.RootPassword, FLAG_ROOTPASSWD, "p", "", "The root password of the cluster") + baseFlags.StringVar(&o.Charset, FLAG_CHARSET, "utf8mb4", "The charset using in ob tenant") + baseFlags.StringVar(&o.ConnectWhiteList, FLAG_CONNECT_WHITE_LIST, "%", "The connect white list using in ob tenant") + baseFlags.StringVar(&o.From, FLAG_FROM, "", "restore from data source") +} + +// AddPoolFlags add pool-related flags +func (o *CreateOptions) AddPoolFlags(cmd *cobra.Command) { + poolFlags := pflag.NewFlagSet(FLAGSET_ZONE, pflag.ContinueOnError) + poolFlags.StringToStringVar(&o.ZonePriority, FLAG_ZONE_PRIORITY, map[string]string{"z1": "1"}, "The zones of the tenant in the format 'Zone=Priority', multiple values can be provided separated by commas") + cmd.Flags().AddFlagSet(poolFlags) +} + +// AddUnitFlags add unit-resource-related flags +func (o *CreateOptions) AddUnitFlags(cmd *cobra.Command) { + unitFlags := pflag.NewFlagSet(FLAGSET_UNIT, pflag.ContinueOnError) + unitFlags.IntVar(&o.UnitNumber, FLAG_UNIT_NUMBER, 1, "unit number of the OBTenant") + unitFlags.Int64Var(&o.UnitConfig.MaxIops, FLAG_MAX_IOPS, 1024, "The max iops of unit") + unitFlags.Int64Var(&o.UnitConfig.MinIops, FLAG_MIN_IOPS, 1024, "The min iops of unit") + unitFlags.IntVar(&o.UnitConfig.IopsWeight, FLAG_IOPS_WEIGHT, 1, "The iops weight of unit") + unitFlags.StringVar(&o.UnitConfig.CPUCount, FLAG_CPU_COUNT, "1", "The cpu count of unit") + unitFlags.StringVar(&o.UnitConfig.MemorySize, FLAG_MEMORY_SIZE, "2Gi", "The memory size of unit") + unitFlags.StringVar(&o.UnitConfig.LogDiskSize, FLAG_LOG_DISK_SIZE, "4Gi", "The log disk size of unit") + cmd.Flags().AddFlagSet(unitFlags) +} + +// AddRestoreFlags add restore flags +func (o *CreateOptions) AddRestoreFlags(cmd *cobra.Command) { + restoreFlags := pflag.NewFlagSet(FLAGSET_RESTORE, pflag.ContinueOnError) + restoreFlags.BoolVarP(&o.Restore, FLAG_RESTORE, "r", false, "Restore from backup files") + restoreFlags.StringVar(&o.RestoreType, FLAG_RESTORE_TYPE, "OSS", "The type of restore source, support OSS or NFS") + restoreFlags.StringVar(&o.Source.Restore.ArchiveSource, FLAG_ARCHIVE_SOURCE, "demo_tenant/log_archive_custom", "The archive source of restore") + restoreFlags.StringVar(&o.Source.Restore.BakEncryptionPassword, FLAG_BAK_ENCRYPTION_PASS, "", "The backup encryption password of obtenant") + restoreFlags.StringVar(&o.Source.Restore.BakDataSource, FLAG_BAK_DATA_SOURCE, "demo_tenant/data_backup_custom_enc", "The bak data source of restore") + restoreFlags.StringVar(&o.Source.Restore.OSSAccessID, FLAG_OSS_ACCESS_ID, "", "The oss access id of restore") + restoreFlags.StringVar(&o.Source.Restore.OSSAccessKey, FLAG_OSS_ACCESS_KEY, "", "The oss access key of restore") + restoreFlags.BoolVar(&o.Source.Restore.Until.Unlimited, FLAG_UNLIMITED, true, "time limited for restore") + restoreFlags.StringVar(&o.Timestamp, FLAG_UNTIL_TIMESTAMP, "", "timestamp for obtenant restore") + cmd.Flags().AddFlagSet(restoreFlags) +} diff --git a/internal/cli/tenant/delete.go b/internal/cli/tenant/delete.go new file mode 100644 index 000000000..f443b8469 --- /dev/null +++ b/internal/cli/tenant/delete.go @@ -0,0 +1,33 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + + "github.com/oceanbase/ob-operator/internal/cli/generic" +) + +type DeleteOptions struct { + generic.ResourceOption +} + +func NewDeleteOptions() *DeleteOptions { + return &DeleteOptions{} +} + +// AddFlags add basic flags for tenant management +func (o *DeleteOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of ob tenant") +} diff --git a/internal/cli/tenant/enter.go b/internal/cli/tenant/enter.go new file mode 100644 index 000000000..6be0faef7 --- /dev/null +++ b/internal/cli/tenant/enter.go @@ -0,0 +1,55 @@ +/* +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 tenant + +const ( + // Flagsets for tenant + FLAGSET_UNIT = "unit" + FLAGSET_RESTORE = "restore" + FLAGSET_ZONE = "zone" + + // Basic Flags + FLAG_TENANT_NAME = "tenant-name" + FLAG_CLUSTER_NAME = "cluster" + FLAG_NAMESPACE = "namespace" + + // Other Flags + FLAG_ROOTPASSWD = "root-password" + FLAG_FORCE = "force" + FLAG_CHARSET = "charset" + FLAG_CONNECT_WHITE_LIST = "connect-white-list" + FLAG_FROM = "from" + FLAG_ZONE_PRIORITY = "priority" + + // unit-resource-related flags + FLAG_UNIT_NUMBER = "unit-number" + FLAG_MAX_IOPS = "max-iops" + FLAG_MIN_IOPS = "min-iops" + FLAG_IOPS_WEIGHT = "iops-weight" + FLAG_CPU_COUNT = "cpu-count" + FLAG_MEMORY_SIZE = "memory-size" + FLAG_LOG_DISK_SIZE = "log-disk-size" + + // restore flags + FLAG_RESTORE = "restore" + FLAG_RESTORE_TYPE = "type" + FLAG_ARCHIVE_SOURCE = "archive-source" + FLAG_BAK_ENCRYPTION_PASS = "bak-encryption-password" + FLAG_BAK_DATA_SOURCE = "bak-data-source" + FLAG_OSS_ACCESS_ID = "oss-access-id" + FLAG_OSS_ACCESS_KEY = "oss-access-key" + FLAG_UNLIMITED = "unlimited" + FLAG_UNTIL_TIMESTAMP = "until-timestamp" + FLAG_PASSWD = "password" +) diff --git a/internal/cli/tenant/list.go b/internal/cli/tenant/list.go new file mode 100644 index 000000000..8ba9238d1 --- /dev/null +++ b/internal/cli/tenant/list.go @@ -0,0 +1,34 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + + "github.com/oceanbase/ob-operator/internal/cli/generic" +) + +type ListOptions struct { + generic.ResourceOption + ClusterName string +} + +func NewListOptions() *ListOptions { + return &ListOptions{} +} + +func (o *ListOptions) AddFlags(cmd *cobra.Command) { + // These flags are used for further list options, but not used yet. + cmd.Flags().StringVar(&o.ClusterName, FLAG_CLUSTER_NAME, "", "The cluster name tenant belonged to in k8s") +} diff --git a/internal/cli/tenant/replaylog.go b/internal/cli/tenant/replaylog.go new file mode 100644 index 000000000..d4ec38786 --- /dev/null +++ b/internal/cli/tenant/replaylog.go @@ -0,0 +1,79 @@ +/* +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 tenant + +import ( + "errors" + + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" + oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" +) + +type ReplayLogOptions struct { + generic.ResourceOption + RestoreUntilOptions + force bool +} +type RestoreUntilOptions struct { + Timestamp string `json:"timestamp,omitempty" example:"2024-02-23 17:47:00"` + Unlimited bool `json:"unlimited,omitempty"` +} + +func NewReplayLogOptions() *ReplayLogOptions { + return &ReplayLogOptions{} +} + +func GetReplayLogOperation(o *ReplayLogOptions) *v1alpha1.OBTenantOperation { + replayLogOp := v1alpha1.OBTenantOperation{ + ObjectMeta: v1.ObjectMeta{ + Name: o.Name + "-replay-log-" + rand.String(6), + Namespace: o.Namespace, + Labels: map[string]string{oceanbaseconst.LabelRefOBTenantOp: o.Name}, + }, + Spec: v1alpha1.OBTenantOperationSpec{ + Type: apiconst.TenantOpReplayLog, + ReplayUntil: &v1alpha1.RestoreUntilConfig{ + Timestamp: &o.Timestamp, + Unlimited: o.Unlimited, + }, + TargetTenant: &o.Name, + Force: o.force, + }, + } + return &replayLogOp +} + +func (o *ReplayLogOptions) Validate() error { + if o.Namespace == "" { + return errors.New("namespace is not specified") + } + if !o.Unlimited && o.Timestamp != "" { + return errors.New("timestamp is required if the restore is limited") + } + return nil +} + +// AddFlags add basic flags for tenant management +func (o *ReplayLogOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "The namespace of OBTenant") + cmd.Flags().BoolVarP(&o.force, FLAG_FORCE, "f", false, "force operation") + cmd.Flags().StringVar(&o.RestoreUntilOptions.Timestamp, FLAG_UNTIL_TIMESTAMP, "", "timestamp for obtenant restore,example: 2024-02-23 17:47:00") + cmd.Flags().BoolVar(&o.RestoreUntilOptions.Unlimited, FLAG_UNLIMITED, true, "time limit for obtenant restore") +} diff --git a/internal/cli/tenant/scale.go b/internal/cli/tenant/scale.go new file mode 100644 index 000000000..39697dcfb --- /dev/null +++ b/internal/cli/tenant/scale.go @@ -0,0 +1,136 @@ +/* +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 tenant + +import ( + "errors" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" + "github.com/oceanbase/ob-operator/internal/cli/utils" + oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" + param "github.com/oceanbase/ob-operator/internal/dashboard/model/param" +) + +type ScaleOptions struct { + generic.ResourceOption + ScaleType string + UnitNumber int + force bool + // Operation config + UnitConfig *param.UnitConfig `json:"unitConfig" binding:"required"` + OldResourcePools []v1alpha1.ResourcePoolSpec `json:"oldResourcePools,omitempty"` + ModifyResourcePools []v1alpha1.ResourcePoolSpec `json:"modifyResourcePools,omitempty"` +} + +func NewScaleOptions() *ScaleOptions { + return &ScaleOptions{ + UnitConfig: ¶m.UnitConfig{}, + OldResourcePools: make([]v1alpha1.ResourcePoolSpec, 0), + ModifyResourcePools: make([]v1alpha1.ResourcePoolSpec, 0), + } +} + +// GetScaleOperation creates scale opertaion +func GetScaleOperation(o *ScaleOptions) *v1alpha1.OBTenantOperation { + scaleOp := &v1alpha1.OBTenantOperation{ + ObjectMeta: v1.ObjectMeta{ + Name: o.Name + "-scale-" + rand.String(6), + Namespace: o.Namespace, + Labels: map[string]string{oceanbaseconst.LabelRefOBTenantOp: o.Name}, + }, + Spec: v1alpha1.OBTenantOperationSpec{ + TargetTenant: &o.Name, + Force: o.force, + }, + } + switch o.ScaleType { + case "unit-number": + scaleOp.Spec.Type = apiconst.TenantOpSetUnitNumber + scaleOp.Spec.UnitNumber = o.UnitNumber + case "unit-config": + scaleOp.Spec.Type = apiconst.TenantOpModifyResourcePools + scaleOp.Spec.ModifyResourcePools = o.ModifyResourcePools + } + + return scaleOp +} + +func (o *ScaleOptions) Complete() error { + unitConfig, err := utils.ParseUnitConfig(o.UnitConfig) + if err != nil { + return err + } + switch o.ScaleType { + case "unit-config": + for _, pool := range o.OldResourcePools { + poolConfig := pool.DeepCopy() + poolConfig.UnitConfig = unitConfig + o.ModifyResourcePools = append(o.ModifyResourcePools, *poolConfig) + } + case "addPrimaryZones", "deletePrimaryZones": + // TODO: add primaryZone and delete primaryZone + default: + } + return nil +} + +func (o *ScaleOptions) Validate() error { + typeCount := 0 + unitFlags := []string{"max-iops", "min-iops", "iops-weight", "cpu-count", "memory-size", "log-disk-size"} + if o.CheckIfFlagChanged(unitFlags...) { + o.ScaleType = "unit-config" + typeCount++ + } + if o.CheckIfFlagChanged("unit-number") { + o.ScaleType = "unit-number" + typeCount++ + } + if typeCount > 1 { + return errors.New("Only one type of scale is allowed at a time") + } + if typeCount == 0 { + return errors.New("No scale type specified") + } + if o.ScaleType == "unit-number" && o.UnitNumber < 1 { + return errors.New("unit number must be greater than one") + } + return nil +} + +// AddFlags for scale options +func (o *ScaleOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of OBTenant") + cmd.Flags().IntVar(&o.UnitNumber, FLAG_UNIT_NUMBER, 1, "unit-number of pools") + cmd.Flags().BoolVarP(&o.force, FLAG_FORCE, "f", false, "force operation") + o.AddUnitFlags(cmd) +} + +// AddUnitFlags add unit-resource-related flags +func (o *ScaleOptions) AddUnitFlags(cmd *cobra.Command) { + unitFlags := pflag.NewFlagSet(FLAGSET_UNIT, pflag.ContinueOnError) + unitFlags.Int64Var(&o.UnitConfig.MaxIops, FLAG_MAX_IOPS, 1024, "The max iops of unit") + unitFlags.Int64Var(&o.UnitConfig.MinIops, FLAG_MIN_IOPS, 1024, "The min iops of unit") + unitFlags.IntVar(&o.UnitConfig.IopsWeight, FLAG_IOPS_WEIGHT, 1, "The iops weight of unit") + unitFlags.StringVar(&o.UnitConfig.CPUCount, FLAG_CPU_COUNT, "1", "The cpu count of unit") + unitFlags.StringVar(&o.UnitConfig.MemorySize, FLAG_MEMORY_SIZE, "2Gi", "The memory size of unit") + unitFlags.StringVar(&o.UnitConfig.LogDiskSize, FLAG_LOG_DISK_SIZE, "4Gi", "The log disk size of unit") + cmd.Flags().AddFlagSet(unitFlags) +} diff --git a/internal/cli/tenant/show.go b/internal/cli/tenant/show.go new file mode 100644 index 000000000..8e21fc8a6 --- /dev/null +++ b/internal/cli/tenant/show.go @@ -0,0 +1,33 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + + "github.com/oceanbase/ob-operator/internal/cli/generic" +) + +type ShowOptions struct { + generic.ResourceOption +} + +func NewShowOptions() *ShowOptions { + return &ShowOptions{} +} + +// AddFlags add basic flags for tenant management +func (o *ShowOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of ob cluster") +} diff --git a/internal/cli/tenant/switchover.go b/internal/cli/tenant/switchover.go new file mode 100644 index 000000000..da1d7bd24 --- /dev/null +++ b/internal/cli/tenant/switchover.go @@ -0,0 +1,67 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" + oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" +) + +type SwitchOverOptions struct { + generic.ResourceOption + force bool + PrimaryTenant string + StandbyTenant string +} + +func NewSwitchOverOptions() *SwitchOverOptions { + return &SwitchOverOptions{} +} + +func (o *SwitchOverOptions) Parse(_ *cobra.Command, args []string) error { + o.PrimaryTenant = args[0] + o.StandbyTenant = args[1] + return nil +} + +func GetSwitchOverOperation(o *SwitchOverOptions) *v1alpha1.OBTenantOperation { + switchOverOp := &v1alpha1.OBTenantOperation{ + ObjectMeta: v1.ObjectMeta{ + Name: o.PrimaryTenant + "-switchover-" + rand.String(6), + Namespace: o.Namespace, + Labels: map[string]string{oceanbaseconst.LabelRefOBTenantOp: o.PrimaryTenant}, + }, + Spec: v1alpha1.OBTenantOperationSpec{ + Type: apiconst.TenantOpSwitchover, + Switchover: &v1alpha1.OBTenantOpSwitchoverSpec{ + PrimaryTenant: o.PrimaryTenant, + StandbyTenant: o.StandbyTenant, + }, + Force: o.force, + }, + } + return switchOverOp +} + +// AddFlags add basic flags for tenant management +func (o *SwitchOverOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "namespace of ob tenant") + cmd.Flags().BoolVarP(&o.force, FLAG_FORCE, "f", false, "force operation") +} diff --git a/internal/cli/tenant/update.go b/internal/cli/tenant/update.go new file mode 100644 index 000000000..d6614e4a1 --- /dev/null +++ b/internal/cli/tenant/update.go @@ -0,0 +1,213 @@ +/* +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 tenant + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" + "github.com/oceanbase/ob-operator/internal/cli/utils" + oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" + param "github.com/oceanbase/ob-operator/internal/dashboard/model/param" +) + +type UpdateOptions struct { + generic.ResourceOption + // flags for cli + force bool + Pools []param.ResourcePoolSpec `json:"pools" binding:"required"` + ConnectWhiteList string `json:"connectWhiteList,omitempty"` + ZonePriority map[string]string `json:"zonePriority"` + UpdateType string `json:"updateType"` + UnitConfig *param.UnitConfig `json:"unitConfig" binding:"required"` + // DS for Operation config + OldResourcePools []v1alpha1.ResourcePoolSpec `json:"oldResourcePools,omitempty"` + ModifyResourcePools []v1alpha1.ResourcePoolSpec `json:"modifyResourcePools,omitempty"` + AddResourcePools []v1alpha1.ResourcePoolSpec `json:"addResourcePools,omitempty"` + DeleteResourcePools []string `json:"deleteResourcePools,omitempty"` +} + +func NewUpdateOptions() *UpdateOptions { + return &UpdateOptions{ + ZonePriority: make(map[string]string), + Pools: make([]param.ResourcePoolSpec, 0), + ModifyResourcePools: make([]v1alpha1.ResourcePoolSpec, 0), + AddResourcePools: make([]v1alpha1.ResourcePoolSpec, 0), + DeleteResourcePools: make([]string, 0), + UnitConfig: ¶m.UnitConfig{}, + } +} +func (o *UpdateOptions) Parse(cmd *cobra.Command, args []string) error { + o.Name = args[0] + o.Cmd = cmd + if o.CheckIfFlagChanged("priority") { + pools, err := utils.MapZonesToPools(o.ZonePriority) + if err != nil { + return err + } + o.Pools = pools + } + return nil +} + +func (o *UpdateOptions) Complete() error { + unitConfig, err := utils.ParseUnitConfig(o.UnitConfig) + if err != nil { + return err + } + switch o.UpdateType { + case "addPools": + for _, pool := range o.Pools { + poolConfig := o.CreateResourcePoolSpec(pool, unitConfig) + o.AddResourcePools = append(o.AddResourcePools, *poolConfig) + } + case "deletePools": + for _, pool := range o.Pools { + o.DeleteResourcePools = append(o.DeleteResourcePools, pool.Zone) + } + case "adjustPools": + for _, pool := range o.Pools { + for _, obpool := range o.OldResourcePools { + if obpool.Zone == pool.Zone { + poolConfig := o.CreateResourcePoolSpec(pool, obpool.UnitConfig) + o.ModifyResourcePools = append(o.ModifyResourcePools, *poolConfig) + break + } + } + } + } + + return nil +} + +// CreateResourcePoolSpec Creates ResourcePoolSpec for tenant scale and update +func (o *UpdateOptions) CreateResourcePoolSpec(pool param.ResourcePoolSpec, unitConfig *v1alpha1.UnitConfig) *v1alpha1.ResourcePoolSpec { + return &v1alpha1.ResourcePoolSpec{ + Zone: pool.Zone, + Priority: pool.Priority, + Type: &v1alpha1.LocalityType{ + Name: o.Name, + Replica: 1, + IsActive: true, + }, + UnitConfig: unitConfig, + } +} + +func GetUpdateOperation(o *UpdateOptions) *v1alpha1.OBTenantOperation { + updateOp := &v1alpha1.OBTenantOperation{ + ObjectMeta: v1.ObjectMeta{ + Name: o.Name + "-update-" + rand.String(6), + Namespace: o.Namespace, + Labels: map[string]string{oceanbaseconst.LabelRefOBTenantOp: o.Name}, + }, + Spec: v1alpha1.OBTenantOperationSpec{ + TargetTenant: &o.Name, + Force: o.force, + }, + } + switch o.UpdateType { + case "connect-white-list": + updateOp.Spec.ConnectWhiteList = o.ConnectWhiteList + updateOp.Spec.Type = apiconst.TenantOpSetConnectWhiteList + case "addPools": + updateOp.Spec.AddResourcePools = o.AddResourcePools + updateOp.Spec.Type = apiconst.TenantOpAddResourcePools + case "adjustPools": + updateOp.Spec.ModifyResourcePools = o.ModifyResourcePools + updateOp.Spec.Type = apiconst.TenantOpModifyResourcePools + case "deletedPools": + updateOp.Spec.DeleteResourcePools = o.DeleteResourcePools + updateOp.Spec.Type = apiconst.TenantOpDeleteResourcePools + } + return updateOp +} + +func (o *UpdateOptions) Validate() error { + deleteNum := 0 + zoneNum := len(o.OldResourcePools) + maxDeleteNum := zoneNum - 1 + typeMap := make(map[string]bool) + updateTypeMap := func(name string) { + if !typeMap[name] { + typeMap[name] = true + o.UpdateType = name + } + } + if o.CheckIfFlagChanged("connect-white-list") { + updateTypeMap("connect-white-list") + } + if o.CheckIfFlagChanged("priority") && o.Pools != nil { + found := false + for _, pool := range o.Pools { + for _, obpool := range o.OldResourcePools { + if obpool.Zone == pool.Zone { + found = true + // priority set to 0 -> delete zone + if pool.Priority == 0 { + updateTypeMap("deletePools") + deleteNum++ + } else { + updateTypeMap("adjustPools") + } + break + } + } + if !found { + updateTypeMap("addPools") + } + if o.UpdateType == "deletedPools" && deleteNum > maxDeleteNum { + return fmt.Errorf("OBTenant should have one zone at least") + } + } + // Count the number of update types specified + typeCount := len(typeMap) + if typeCount > 1 { + return errors.New("Only one type of update is allowed at a time") + } + if typeCount == 0 { + return errors.New("No update type specified") + } + } + return nil +} + +// AddFlags add basic flags for tenant management +func (o *UpdateOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "The namespace of OBTenant") + cmd.Flags().StringVar(&o.ConnectWhiteList, FLAG_CONNECT_WHITE_LIST, "", "The connect white list using in ob tenant") + cmd.Flags().StringToStringVar(&o.ZonePriority, FLAG_ZONE_PRIORITY, nil, "zone priority config of OBTenant") + cmd.Flags().BoolVarP(&o.force, FLAG_FORCE, "f", false, "force operation") + o.AddUnitFlags(cmd) +} + +// AddUnitFlags add unit-resource-related flags +func (o *UpdateOptions) AddUnitFlags(cmd *cobra.Command) { + unitFlags := pflag.NewFlagSet(FLAGSET_UNIT, pflag.ContinueOnError) + unitFlags.Int64Var(&o.UnitConfig.MaxIops, FLAG_MAX_IOPS, 1024, "The max iops of unit") + unitFlags.Int64Var(&o.UnitConfig.MinIops, FLAG_MIN_IOPS, 1024, "The min iops of unit") + unitFlags.IntVar(&o.UnitConfig.IopsWeight, FLAG_IOPS_WEIGHT, 1, "The iops weight of unit") + unitFlags.StringVar(&o.UnitConfig.CPUCount, FLAG_CPU_COUNT, "1", "The cpu count of unit") + unitFlags.StringVar(&o.UnitConfig.MemorySize, FLAG_MEMORY_SIZE, "2Gi", "The memory size of unit") + unitFlags.StringVar(&o.UnitConfig.LogDiskSize, FLAG_LOG_DISK_SIZE, "4Gi", "The log disk size of unit") + cmd.Flags().AddFlagSet(unitFlags) +} diff --git a/internal/cli/tenant/upgrade.go b/internal/cli/tenant/upgrade.go new file mode 100644 index 000000000..19939d191 --- /dev/null +++ b/internal/cli/tenant/upgrade.go @@ -0,0 +1,56 @@ +/* +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 tenant + +import ( + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + + apiconst "github.com/oceanbase/ob-operator/api/constants" + "github.com/oceanbase/ob-operator/api/v1alpha1" + "github.com/oceanbase/ob-operator/internal/cli/generic" + oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" +) + +type UpgradeOptions struct { + generic.ResourceOption + force bool +} + +func NewUpgradeOptions() *UpgradeOptions { + return &UpgradeOptions{} +} + +func GetUpgradeOperation(o *UpgradeOptions) *v1alpha1.OBTenantOperation { + upgradeOp := &v1alpha1.OBTenantOperation{ + ObjectMeta: v1.ObjectMeta{ + Name: o.Name + "-upgrade-" + rand.String(6), + Namespace: o.Namespace, + Labels: map[string]string{oceanbaseconst.LabelRefOBTenantOp: o.Name}, + }, + Spec: v1alpha1.OBTenantOperationSpec{ + Type: apiconst.TenantOpUpgrade, + TargetTenant: &o.Name, + Force: o.force, + }, + } + return upgradeOp +} + +// AddFlags add basic flags for tenant management +func (o *UpgradeOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Namespace, FLAG_NAMESPACE, "default", "The namespace of the tenant") + cmd.Flags().BoolVarP(&o.force, FLAG_FORCE, "f", false, "force operation") +} diff --git a/internal/cli/utils/utils.go b/internal/cli/utils/utils.go index f19da7b01..79aa7a2ea 100644 --- a/internal/cli/utils/utils.go +++ b/internal/cli/utils/utils.go @@ -16,15 +16,18 @@ package utils import ( "crypto/rand" "fmt" - "log" + "math" "regexp" "strconv" "strings" "time" + "k8s.io/apimachinery/pkg/api/resource" k8srand "k8s.io/apimachinery/pkg/util/rand" apitypes "github.com/oceanbase/ob-operator/api/types" + "github.com/oceanbase/ob-operator/api/v1alpha1" + oberr "github.com/oceanbase/ob-operator/pkg/errors" "github.com/oceanbase/ob-operator/internal/dashboard/model/common" @@ -32,7 +35,7 @@ import ( ) const ( - characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#%^&*_-+=|(){}[]:;,.?/`$\"<>" + characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~#%^&*_-+|(){}[]:;,.?/\"" factor = 4294901759 ) @@ -45,27 +48,23 @@ func GenerateUserSecrets(clusterName string, clusterId int64) *apitypes.OBUserSe } } -// GenerateClusterId generated random cluster id -func GenerateClusterId() int64 { - clusterId := time.Now().Unix() % factor - if clusterId != 0 { - return clusterId +// GenerateClusterID generated random cluster ID +func GenerateClusterID() int64 { + clusterID := time.Now().Unix() % factor + if clusterID != 0 { + return clusterID } - return GenerateClusterId() + return GenerateClusterID() } +// CheckResourceName checks resource name in k8s func CheckResourceName(name string) bool { regex := `[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*` - - re, err := regexp.Compile(regex) - if err != nil { - log.Println("Error compiling regex:", err) - return false - } - + re := regexp.MustCompile(regex) return re.MatchString(name) } +// CheckPassword checks password when creating cluster func CheckPassword(password string) bool { var ( countUppercase int @@ -97,10 +96,17 @@ func CheckPassword(password string) bool { return countUppercase >= 2 && countLowercase >= 2 && countNumber >= 2 && countSpecialChar >= 2 } +// CheckTenantName check Tenant name when creating tenant +func CheckTenantName(name string) bool { + regex := `^[_a-zA-Z][^-]*$` + re := regexp.MustCompile(regex) + return re.MatchString(name) +} + // MapZonesToTopology map --zones to zoneTopology func MapZonesToTopology(zones map[string]string) ([]param.ZoneTopology, error) { if zones == nil { - return nil, fmt.Errorf("Zone value is required") // 无效的zone信息 + return nil, fmt.Errorf("Zone replica is required") } topology := make([]param.ZoneTopology, 0) for zoneName, replicaStr := range zones { @@ -119,6 +125,38 @@ func MapZonesToTopology(zones map[string]string) ([]param.ZoneTopology, error) { return topology, nil } +// MapZonesToPools map --zones to []resourcePool +func MapZonesToPools(zones map[string]string) ([]param.ResourcePoolSpec, error) { + if zones == nil { + return nil, fmt.Errorf("Zone priority is required") + } + resourcePool := make([]param.ResourcePoolSpec, 0) + for zoneName, priorityStr := range zones { + priority, err := strconv.Atoi(priorityStr) + if err != nil { + return nil, fmt.Errorf("invalid value for zone %s: %s", zoneName, priorityStr) + } + resourcePool = append(resourcePool, param.ResourcePoolSpec{ + Zone: zoneName, + Priority: priority, + Type: "Full", + }) + } + return resourcePool, nil +} + +// MapParameters map --parameters to parameters +func MapParameters(parameters map[string]string) ([]common.KVPair, error) { + kvMap := make([]common.KVPair, 0) + for k, v := range parameters { + kvMap = append(kvMap, common.KVPair{ + Key: k, + Value: v, + }) + } + return kvMap, nil +} + // GenerateRandomPassword generated random password in range [minLength,maxLength] func GenerateRandomPassword(minLength int, maxLength int) string { const ( @@ -164,6 +202,42 @@ func GenerateRandomPassword(minLength int, maxLength int) string { return sb.String() } +// ParseUnitConfig parse param.UnitConfig to v1alpha1.UnitConfig +func ParseUnitConfig(unitConfig *param.UnitConfig) (*v1alpha1.UnitConfig, error) { + cpuCount, err := resource.ParseQuantity(unitConfig.CPUCount) + if err != nil { + return nil, oberr.NewBadRequest("invalid cpu count: " + err.Error()) + } + memorySize, err := resource.ParseQuantity(unitConfig.MemorySize) + if err != nil { + return nil, oberr.NewBadRequest("invalid memory size: " + err.Error()) + } + logDiskSize, err := resource.ParseQuantity(unitConfig.LogDiskSize) + if err != nil { + return nil, oberr.NewBadRequest("invalid log disk size: " + err.Error()) + } + var maxIops, minIops int + if unitConfig.MaxIops > math.MaxInt32 { + maxIops = math.MaxInt32 + } else { + maxIops = int(unitConfig.MaxIops) + } + if unitConfig.MinIops > math.MaxInt32 { + minIops = math.MaxInt32 + } else { + minIops = int(unitConfig.MinIops) + } + return &v1alpha1.UnitConfig{ + MaxCPU: cpuCount, + MemorySize: memorySize, + MinCPU: cpuCount, + LogDiskSize: logDiskSize, + MaxIops: maxIops, + MinIops: minIops, + IopsWeight: unitConfig.IopsWeight, + }, nil +} + // GenerateUUID returns uuid func GenerateUUID() string { return k8srand.String(12) diff --git a/internal/clients/obtenant.go b/internal/clients/obtenant.go index 4d9cb9a31..47d1bef6e 100644 --- a/internal/clients/obtenant.go +++ b/internal/clients/obtenant.go @@ -14,14 +14,17 @@ package clients import ( "context" + "fmt" "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "github.com/oceanbase/ob-operator/api/v1alpha1" "github.com/oceanbase/ob-operator/internal/clients/schema" oceanbaseconst "github.com/oceanbase/ob-operator/internal/const/oceanbase" + "github.com/oceanbase/ob-operator/pkg/k8s/client" ) func CreateOBTenant(ctx context.Context, tenant *v1alpha1.OBTenant) (*v1alpha1.OBTenant, error) { @@ -53,6 +56,22 @@ func CreateOBTenantOperation(ctx context.Context, op *v1alpha1.OBTenantOperation return OperationClient.Create(ctx, op, metav1.CreateOptions{}) } +func GetOBTenantOperations(ctx context.Context, obtenant *v1alpha1.OBTenant) (*v1alpha1.OBTenantOperationList, error) { + client := client.GetClient() + var obtenantOperationList v1alpha1.OBTenantOperationList + obj, err := client.DynamicClient.Resource(schema.OBTenantOperationGVR).Namespace(obtenant.Namespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", oceanbaseconst.LabelRefOBTenantOp, obtenant.Name), + }) + if err != nil { + return nil, errors.Wrap(err, "List obtenant operations") + } + err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &obtenantOperationList) + if err != nil { + return nil, errors.Wrap(err, "Convert unstructured to obtenant list") + } + return &obtenantOperationList, nil +} + func GetTenantBackupPolicy(ctx context.Context, nn types.NamespacedName) (*v1alpha1.OBTenantBackupPolicy, error) { policyListOptions := metav1.ListOptions{ LabelSelector: oceanbaseconst.LabelTenantName + "=" + nn.Name, diff --git a/internal/const/oceanbase/labels.go b/internal/const/oceanbase/labels.go index 48d48db58..99204537f 100644 --- a/internal/const/oceanbase/labels.go +++ b/internal/const/oceanbase/labels.go @@ -20,6 +20,7 @@ const ( LabelJobName = "job-name" LabelRefBackupPolicy = "ref-backuppolicy" LabelRefOBClusterOp = "ref-obclusterop" + LabelRefOBTenantOp = "ref-obtenantop" ) const (