github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/migration/base.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package migration
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"os"
    26  	"strings"
    27  
    28  	"k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/client-go/dynamic"
    33  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    34  
    35  	"github.com/1aal/kubeblocks/pkg/cli/types"
    36  	migrationv1 "github.com/1aal/kubeblocks/pkg/cli/types/migrationapi"
    37  	"github.com/1aal/kubeblocks/pkg/cli/util"
    38  )
    39  
    40  const (
    41  	MigrationTaskLabel          = "datamigration.apecloud.io/migrationtask"
    42  	MigrationTaskStepAnnotation = "datamigration.apecloud.io/step"
    43  	SerialJobOrderAnnotation    = "common.apecloud.io/serial_job_order"
    44  )
    45  
    46  const (
    47  	invalidMigrationCrdAdvice = "to use migration-related functions, please ensure that the addon of migration is enabled, use: 'kbcli addon enable migration' to enable the addon"
    48  )
    49  
    50  // Endpoint
    51  // Todo: For the source or target is cluster in KubeBlocks. A better way is to get secret from {$clustername}-conn-credential, so the username, password, addresses can be omitted
    52  
    53  type EndpointModel struct {
    54  	UserName string `json:"userName"`
    55  	Password string `json:"password"`
    56  	Address  string `json:"address"`
    57  	// +optional
    58  	Database string `json:"databaseName,omitempty"`
    59  }
    60  
    61  func (e *EndpointModel) BuildFromStr(msgArr *[]string, endpointStr string) error {
    62  	if endpointStr == "" {
    63  		BuildErrorMsg(msgArr, "endpoint string cannot be empty")
    64  		return nil
    65  	}
    66  	e.clear()
    67  	endpointStr = strings.TrimSpace(endpointStr)
    68  	accountURLPair := strings.Split(endpointStr, "@")
    69  	if len(accountURLPair) != 2 {
    70  		BuildErrorMsg(msgArr, "endpoint may not contain account info")
    71  		return nil
    72  	}
    73  	accountPair := strings.Split(accountURLPair[0], ":")
    74  	if len(accountPair) != 2 {
    75  		BuildErrorMsg(msgArr, "the account info in endpoint is invalid, should be like \"user:123456\"")
    76  		return nil
    77  	}
    78  	e.UserName = accountPair[0]
    79  	e.Password = accountPair[1]
    80  	if strings.LastIndex(accountURLPair[1], "/") != -1 {
    81  		addressDatabasePair := strings.Split(accountURLPair[1], "/")
    82  		e.Address = strings.Join(addressDatabasePair[:len(addressDatabasePair)-1], "/")
    83  		e.Database = addressDatabasePair[len(addressDatabasePair)-1]
    84  	} else {
    85  		e.Address = accountURLPair[1]
    86  	}
    87  	return nil
    88  }
    89  
    90  func (e *EndpointModel) clear() {
    91  	e.Address = ""
    92  	e.Password = ""
    93  	e.UserName = ""
    94  	e.Database = ""
    95  }
    96  
    97  // Migration Object
    98  
    99  type MigrationObjectModel struct {
   100  	WhiteList []DBObjectExpress `json:"whiteList"`
   101  }
   102  
   103  type DBObjectExpress struct {
   104  	SchemaName string `json:"schemaName"`
   105  	// +optional
   106  	IsAll bool `json:"isAll"`
   107  	// +optional
   108  	TableList []TableObjectExpress `json:"tableList"`
   109  }
   110  
   111  type TableObjectExpress struct {
   112  	TableName string `json:"tableName"`
   113  	// +optional
   114  	IsAll bool `json:"isAll"`
   115  }
   116  
   117  func (m *MigrationObjectModel) BuildFromStrs(errMsgArr *[]string, objStrs []string) error {
   118  	if len(objStrs) == 0 {
   119  		BuildErrorMsg(errMsgArr, "migration object cannot be empty")
   120  		return nil
   121  	}
   122  	for _, str := range objStrs {
   123  		msg := ""
   124  		if str == "" {
   125  			msg = "the database or database.table in migration object cannot be empty"
   126  		}
   127  		dbTablePair := strings.Split(str, ".")
   128  		if len(dbTablePair) > 2 {
   129  			msg = fmt.Sprintf("[%s] is not a valid database or database.table", str)
   130  		}
   131  		if msg != "" {
   132  			BuildErrorMsg(errMsgArr, msg)
   133  			return nil
   134  		}
   135  		if len(dbTablePair) == 1 {
   136  			m.WhiteList = append(m.WhiteList, DBObjectExpress{
   137  				SchemaName: str,
   138  				IsAll:      true,
   139  			})
   140  		} else {
   141  			dbObjPoint, err := m.ContainSchema(dbTablePair[0])
   142  			if err != nil {
   143  				return err
   144  			}
   145  			if dbObjPoint != nil {
   146  				dbObjPoint.TableList = append(dbObjPoint.TableList, TableObjectExpress{
   147  					TableName: dbTablePair[1],
   148  					IsAll:     true,
   149  				})
   150  			} else {
   151  				m.WhiteList = append(m.WhiteList, DBObjectExpress{
   152  					SchemaName: dbTablePair[0],
   153  					TableList: []TableObjectExpress{{
   154  						TableName: dbTablePair[1],
   155  						IsAll:     true,
   156  					}},
   157  				})
   158  			}
   159  		}
   160  	}
   161  	return nil
   162  }
   163  
   164  func (m *MigrationObjectModel) ContainSchema(schemaName string) (*DBObjectExpress, error) {
   165  	for i := 0; i < len(m.WhiteList); i++ {
   166  		if m.WhiteList[i].SchemaName == schemaName {
   167  			return &m.WhiteList[i], nil
   168  		}
   169  	}
   170  	return nil, nil
   171  }
   172  
   173  func CliStepChangeToStructure() (map[string]string, []string) {
   174  	validStepMap := map[string]string{
   175  		migrationv1.CliStepPreCheck.String():   migrationv1.CliStepPreCheck.String(),
   176  		migrationv1.CliStepInitStruct.String(): migrationv1.CliStepInitStruct.String(),
   177  		migrationv1.CliStepInitData.String():   migrationv1.CliStepInitData.String(),
   178  		migrationv1.CliStepCdc.String():        migrationv1.CliStepCdc.String(),
   179  	}
   180  	validStepKey := make([]string, 0)
   181  	for k := range validStepMap {
   182  		validStepKey = append(validStepKey, k)
   183  	}
   184  	return validStepMap, validStepKey
   185  }
   186  
   187  type TaskTypeEnum string
   188  
   189  const (
   190  	Initialization       TaskTypeEnum = "initialization"
   191  	InitializationAndCdc TaskTypeEnum = "initialization-and-cdc" // default value
   192  )
   193  
   194  func (s TaskTypeEnum) String() string {
   195  	return string(s)
   196  }
   197  
   198  func IsMigrationCrdValidWithDynamic(dynamic *dynamic.Interface) (bool, error) {
   199  	resource := types.CustomResourceDefinitionGVR()
   200  	if err := APIResource(dynamic, &resource, "migrationtasks.datamigration.apecloud.io", "", nil); err != nil {
   201  		return false, err
   202  	}
   203  	if err := APIResource(dynamic, &resource, "migrationtemplates.datamigration.apecloud.io", "", nil); err != nil {
   204  		return false, err
   205  	}
   206  	if err := APIResource(dynamic, &resource, "serialjobs.common.apecloud.io", "", nil); err != nil {
   207  		return false, err
   208  	}
   209  	return true, nil
   210  }
   211  
   212  func PrintCrdInvalidError(err error) {
   213  	if err == nil {
   214  		return
   215  	}
   216  	if !errors.IsNotFound(err) {
   217  		util.CheckErr(err)
   218  	}
   219  	fmt.Fprintf(os.Stderr, "hint: %s\n", invalidMigrationCrdAdvice)
   220  	os.Exit(cmdutil.DefaultErrorExitCode)
   221  }
   222  
   223  func IsMigrationCrdValidWithFactory(factory cmdutil.Factory) (bool, error) {
   224  	dynamic, err := factory.DynamicClient()
   225  	if err != nil {
   226  		return false, err
   227  	}
   228  	return IsMigrationCrdValidWithDynamic(&dynamic)
   229  }
   230  
   231  func APIResource(dynamic *dynamic.Interface, resource *schema.GroupVersionResource, name string, namespace string, res interface{}) error {
   232  	obj, err := (*dynamic).Resource(*resource).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{}, "")
   233  	if err != nil {
   234  		return err
   235  	}
   236  	if res != nil {
   237  		return runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, res)
   238  	}
   239  	return nil
   240  }
   241  
   242  func BuildErrorMsg(msgArr *[]string, msg string) {
   243  	if *msgArr == nil {
   244  		*msgArr = make([]string, 1)
   245  	}
   246  	*msgArr = append(*msgArr, msg)
   247  }
   248  
   249  func BuildInitializationStepsOrder(task *migrationv1.MigrationTask, template *migrationv1.MigrationTemplate) []string {
   250  	stepMap := make(map[string]string)
   251  	for _, taskStep := range task.Spec.Initialization.Steps {
   252  		stepMap[taskStep.String()] = taskStep.String()
   253  	}
   254  	resultArr := make([]string, 0)
   255  	for _, stepModel := range template.Spec.Initialization.Steps {
   256  		if stepMap[stepModel.Step.String()] != "" {
   257  			resultArr = append(resultArr, stepModel.Step.CliString())
   258  		}
   259  	}
   260  	return resultArr
   261  }