github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/systemaccount_util.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 apps
    21  
    22  import (
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/sethvargo/go-password/password"
    27  	batchv1 "k8s.io/api/batch/v1"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/klog/v2"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  
    33  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    34  	"github.com/1aal/kubeblocks/pkg/constant"
    35  	componetutil "github.com/1aal/kubeblocks/pkg/controller/component"
    36  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    37  )
    38  
    39  // customizedEngine helps render jobs.
    40  type customizedEngine struct {
    41  	cluster       *appsv1alpha1.Cluster
    42  	componentName string
    43  	image         string
    44  	command       []string
    45  	args          []string
    46  	envVarList    []corev1.EnvVar
    47  }
    48  
    49  func (e *customizedEngine) getImage() string {
    50  	return e.image
    51  }
    52  
    53  func (e *customizedEngine) getEnvs() []corev1.EnvVar {
    54  	return e.envVarList
    55  }
    56  
    57  // getPodCommand shows how to execute the sql statement.
    58  // for instance, mysql -h - demo-cluster-replicasets-1 -e  "create user username IDENTIFIED by 'passwd';"
    59  func (e customizedEngine) getCommand() []string {
    60  	return e.command
    61  }
    62  
    63  // getPodCommand shows how to execute the sql statement.
    64  // for instance, mysql -h - demo-cluster-replicasets-1 -e  "create user username IDENTIFIED by 'passwd';"
    65  func (e *customizedEngine) getArgs() []string {
    66  	return e.args
    67  }
    68  
    69  func newCustomizedEngine(execConfig *appsv1alpha1.CmdExecutorConfig, dbcluster *appsv1alpha1.Cluster, compName string) *customizedEngine {
    70  	return &customizedEngine{
    71  		cluster:       dbcluster,
    72  		componentName: compName,
    73  		image:         execConfig.Image,
    74  		command:       execConfig.Command,
    75  		args:          execConfig.Args,
    76  		envVarList:    execConfig.Env,
    77  	}
    78  }
    79  
    80  func replaceEnvsValues(clusterName string, sysAccounts *appsv1alpha1.SystemAccountSpec) {
    81  	namedValuesMap := componetutil.GetEnvReplacementMapForConnCredential(clusterName)
    82  	// replace systemAccounts.cmdExecutorConfig.env[].valueFrom.secretKeyRef.name variables
    83  	cmdConfig := sysAccounts.CmdExecutorConfig
    84  	if cmdConfig != nil {
    85  		cmdConfig.Env = componetutil.ReplaceSecretEnvVars(namedValuesMap, cmdConfig.Env)
    86  	}
    87  
    88  	accounts := sysAccounts.Accounts
    89  	for _, acc := range accounts {
    90  		if acc.ProvisionPolicy.Type == appsv1alpha1.ReferToExisting {
    91  			// replace systemAccounts.accounts[*].provisionPolicy.secretRef.name variables
    92  			secretRef := acc.ProvisionPolicy.SecretRef
    93  			name := componetutil.ReplaceNamedVars(namedValuesMap, secretRef.Name, 1, false)
    94  			if name != secretRef.Name {
    95  				secretRef.Name = name
    96  			}
    97  		}
    98  	}
    99  }
   100  
   101  // getLabelsForSecretsAndJobs constructs matching labels for secrets and jobs.
   102  // This is consistent with that of secrets created during cluster initialization.
   103  func getLabelsForSecretsAndJobs(key componentUniqueKey) client.MatchingLabels {
   104  	return client.MatchingLabels{
   105  		constant.AppInstanceLabelKey:    key.clusterName,
   106  		constant.KBAppComponentLabelKey: key.componentName,
   107  		constant.AppManagedByLabelKey:   constant.AppName,
   108  	}
   109  }
   110  
   111  func renderJob(jobName string, engine *customizedEngine, key componentUniqueKey, statement []string, endpoint string) *batchv1.Job {
   112  	// inject one more system env variables
   113  	statementEnv := corev1.EnvVar{
   114  		Name:  kbAccountStmtEnvName,
   115  		Value: strings.Join(statement, " "),
   116  	}
   117  	endpointEnv := corev1.EnvVar{
   118  		Name:  kbAccountEndPointEnvName,
   119  		Value: endpoint,
   120  	}
   121  	// place statements and endpoints before user defined envs.
   122  	envs := make([]corev1.EnvVar, 0, 2+len(engine.getEnvs()))
   123  	envs = append(envs, statementEnv, endpointEnv)
   124  	if len(engine.getEnvs()) > 0 {
   125  		envs = append(envs, engine.getEnvs()...)
   126  	}
   127  
   128  	job := &batchv1.Job{
   129  		ObjectMeta: metav1.ObjectMeta{
   130  			Namespace: key.namespace,
   131  			Name:      jobName,
   132  		},
   133  		Spec: batchv1.JobSpec{
   134  			Template: corev1.PodTemplateSpec{
   135  				ObjectMeta: metav1.ObjectMeta{
   136  					Namespace: key.namespace,
   137  					Name:      jobName},
   138  				Spec: corev1.PodSpec{
   139  					RestartPolicy: corev1.RestartPolicyNever,
   140  					Containers: []corev1.Container{
   141  						{
   142  							Name:            jobName,
   143  							Image:           engine.getImage(),
   144  							ImagePullPolicy: corev1.PullIfNotPresent,
   145  							Command:         engine.getCommand(),
   146  							Args:            engine.getArgs(),
   147  							Env:             envs,
   148  						},
   149  					},
   150  				},
   151  			},
   152  		},
   153  	}
   154  
   155  	return job
   156  }
   157  
   158  func renderSecretWithPwd(key componentUniqueKey, username, passwd string) *corev1.Secret {
   159  	secretData := map[string][]byte{
   160  		constant.AccountNameForSecret:   []byte(username),
   161  		constant.AccountPasswdForSecret: []byte(passwd),
   162  	}
   163  
   164  	ml := getLabelsForSecretsAndJobs(key)
   165  	ml[constant.ClusterAccountLabelKey] = username
   166  	return renderSecret(key, username, ml, secretData)
   167  }
   168  
   169  func renderSecretByCopy(key componentUniqueKey, username string, fromSecret *corev1.Secret) *corev1.Secret {
   170  	ml := getLabelsForSecretsAndJobs(key)
   171  	ml[constant.ClusterAccountLabelKey] = username
   172  	return renderSecret(key, username, ml, fromSecret.Data)
   173  }
   174  
   175  func renderSecret(key componentUniqueKey, username string, labels client.MatchingLabels, data map[string][]byte) *corev1.Secret {
   176  	// secret labels and secret finalizers should be consistent with that of Cluster secret created by Cluster Controller.
   177  	secret := &corev1.Secret{
   178  		ObjectMeta: metav1.ObjectMeta{
   179  			Namespace:  key.namespace,
   180  			Name:       strings.Join([]string{key.clusterName, key.componentName, username}, "-"),
   181  			Labels:     labels,
   182  			Finalizers: []string{constant.DBClusterFinalizerName},
   183  		},
   184  		Data: data,
   185  	}
   186  	return secret
   187  }
   188  
   189  func retrieveEndpoints(scope appsv1alpha1.ProvisionScope, svcEP *corev1.Endpoints, headlessEP *corev1.Endpoints) []string {
   190  	// parse endpoints
   191  	endpoints := make([]string, 0)
   192  	if scope == appsv1alpha1.AnyPods {
   193  		for _, ss := range svcEP.Subsets {
   194  			for _, add := range ss.Addresses {
   195  				endpoints = append(endpoints, add.IP)
   196  				break
   197  			}
   198  		}
   199  	} else {
   200  		for _, ss := range headlessEP.Subsets {
   201  			for _, add := range ss.Addresses {
   202  				endpoints = append(endpoints, add.IP)
   203  			}
   204  		}
   205  	}
   206  	return endpoints
   207  }
   208  
   209  func getAcctFromSecretAndJobs(secrets *corev1.SecretList, jobs *batchv1.JobList) (detectedFacts appsv1alpha1.KBAccountType) {
   210  	detectedFacts = appsv1alpha1.KBAccountInvalid
   211  	// parse account name from secret's label
   212  	for _, secret := range secrets.Items {
   213  		if accountName, exists := secret.ObjectMeta.Labels[constant.ClusterAccountLabelKey]; exists {
   214  			updateFacts(appsv1alpha1.AccountName(accountName), &detectedFacts)
   215  		}
   216  	}
   217  	// parse account name from job's label
   218  	for _, job := range jobs.Items {
   219  		if accountName, exists := job.ObjectMeta.Labels[constant.ClusterAccountLabelKey]; exists {
   220  			updateFacts(appsv1alpha1.AccountName(accountName), &detectedFacts)
   221  		}
   222  	}
   223  	return
   224  }
   225  
   226  func updateFacts(accountName appsv1alpha1.AccountName, detectedFacts *appsv1alpha1.KBAccountType) {
   227  	switch accountName {
   228  	case appsv1alpha1.AdminAccount:
   229  		*detectedFacts |= appsv1alpha1.KBAccountAdmin
   230  	case appsv1alpha1.DataprotectionAccount:
   231  		*detectedFacts |= appsv1alpha1.KBAccountDataprotection
   232  	case appsv1alpha1.ProbeAccount:
   233  		*detectedFacts |= appsv1alpha1.KBAccountProbe
   234  	case appsv1alpha1.MonitorAccount:
   235  		*detectedFacts |= appsv1alpha1.KBAccountMonitor
   236  	case appsv1alpha1.ReplicatorAccount:
   237  		*detectedFacts |= appsv1alpha1.KBAccountReplicator
   238  	}
   239  }
   240  
   241  func getCreationStmtForAccount(key componentUniqueKey, passConfig appsv1alpha1.PasswordConfig,
   242  	accountConfig appsv1alpha1.SystemAccountConfig, strategy updateStrategy) ([]string, string) {
   243  	// generated password with mixedcases = true
   244  	passwd, _ := password.Generate((int)(passConfig.Length), (int)(passConfig.NumDigits), (int)(passConfig.NumSymbols), false, false)
   245  	// refine password to upper or lower cases w.r.t configuration
   246  	switch passConfig.LetterCase {
   247  	case appsv1alpha1.UpperCases:
   248  		passwd = strings.ToUpper(passwd)
   249  	case appsv1alpha1.LowerCases:
   250  		passwd = strings.ToLower(passwd)
   251  	}
   252  
   253  	userName := (string)(accountConfig.Name)
   254  
   255  	namedVars := getEnvReplacementMapForAccount(userName, passwd)
   256  
   257  	execStmts := make([]string, 0)
   258  
   259  	statements := accountConfig.ProvisionPolicy.Statements
   260  
   261  	if strategy == inPlaceUpdate && len(statements.UpdateStatement) == 0 {
   262  		// if update statement is empty, use reCreate strategy, which will drop and create the account.
   263  		strategy = reCreate
   264  		klog.Warningf("account %s in cluster %s exists, but its update statement is not set, will use %s strategy to update account.", userName, key.clusterName, strategy)
   265  	}
   266  
   267  	if strategy == inPlaceUpdate {
   268  		// use update statement
   269  		stmt := componetutil.ReplaceNamedVars(namedVars, statements.UpdateStatement, -1, true)
   270  		execStmts = append(execStmts, stmt)
   271  	} else {
   272  		// drop if exists + create if not exists
   273  		if len(statements.DeletionStatement) > 0 {
   274  			stmt := componetutil.ReplaceNamedVars(namedVars, statements.DeletionStatement, -1, true)
   275  			execStmts = append(execStmts, stmt)
   276  		}
   277  		stmt := componetutil.ReplaceNamedVars(namedVars, statements.CreationStatement, -1, true)
   278  		execStmts = append(execStmts, stmt)
   279  	}
   280  	// secret := renderSecretWithPwd(key, userName, passwd)
   281  	return execStmts, passwd
   282  }
   283  
   284  func getAllSysAccounts() []appsv1alpha1.AccountName {
   285  	return []appsv1alpha1.AccountName{
   286  		appsv1alpha1.AdminAccount,
   287  		appsv1alpha1.DataprotectionAccount,
   288  		appsv1alpha1.ProbeAccount,
   289  		appsv1alpha1.MonitorAccount,
   290  		appsv1alpha1.ReplicatorAccount,
   291  	}
   292  }
   293  
   294  func getDefaultAccounts() appsv1alpha1.KBAccountType {
   295  	accountID := appsv1alpha1.KBAccountInvalid
   296  	for _, name := range getAllSysAccounts() {
   297  		accountID |= name.GetAccountID()
   298  	}
   299  	return accountID
   300  }
   301  
   302  func getDebugMode(annotatedDebug string) bool {
   303  	debugOn, _ := strconv.ParseBool(annotatedDebug)
   304  	return viper.GetBool(systemAccountsDebugMode) || debugOn
   305  }
   306  
   307  func calibrateJobMetaAndSpec(job *batchv1.Job, cluster *appsv1alpha1.Cluster, compKey componentUniqueKey, account appsv1alpha1.AccountName) error {
   308  	debugModeOn := getDebugMode(cluster.Annotations[debugClusterAnnotationKey])
   309  	// add label
   310  	ml := getLabelsForSecretsAndJobs(compKey)
   311  	ml[constant.ClusterAccountLabelKey] = (string)(account)
   312  	job.ObjectMeta.Labels = ml
   313  
   314  	// if debug mode is on, jobs will retain after execution.
   315  	if debugModeOn {
   316  		job.Spec.TTLSecondsAfterFinished = nil
   317  	} else {
   318  		defaultTTLZero := (int32)(1)
   319  		job.Spec.TTLSecondsAfterFinished = &defaultTTLZero
   320  	}
   321  
   322  	// add toleration
   323  	clusterComp := cluster.Spec.GetComponentByName(compKey.componentName)
   324  	tolerations, err := componetutil.BuildTolerations(cluster, clusterComp)
   325  	if err != nil {
   326  		return err
   327  	}
   328  	job.Spec.Template.Spec.Tolerations = tolerations
   329  
   330  	return nil
   331  }
   332  
   333  // completeExecConfig overrides the image of execConfig if version is not nil.
   334  func completeExecConfig(execConfig *appsv1alpha1.CmdExecutorConfig, version *appsv1alpha1.ClusterComponentVersion) {
   335  	if version == nil || version.SystemAccountSpec == nil || version.SystemAccountSpec.CmdExecutorConfig == nil {
   336  		return
   337  	}
   338  	sysAccountSpec := version.SystemAccountSpec
   339  	if len(sysAccountSpec.CmdExecutorConfig.Image) > 0 {
   340  		execConfig.Image = sysAccountSpec.CmdExecutorConfig.Image
   341  	}
   342  
   343  	// envs from sysAccountSpec will override the envs from execConfig
   344  	if sysAccountSpec.CmdExecutorConfig.Env == nil {
   345  		return
   346  	}
   347  	if len(sysAccountSpec.CmdExecutorConfig.Env) == 0 {
   348  		// clean up envs
   349  		execConfig.Env = nil
   350  	} else {
   351  		execConfig.Env = sysAccountSpec.CmdExecutorConfig.Env
   352  	}
   353  }