pkg/resources/statefulset/mysqld_statefulset.go (265 lines of code) (raw):
// Copyright (c) 2020, 2023, Oracle and/or its affiliates.
//
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
package statefulset
import (
"strconv"
"github.com/mysql/ndb-operator/config/debug"
"github.com/mysql/ndb-operator/pkg/apis/ndbcontroller"
v1 "github.com/mysql/ndb-operator/pkg/apis/ndbcontroller/v1"
"github.com/mysql/ndb-operator/pkg/constants"
"github.com/mysql/ndb-operator/pkg/helpers"
"github.com/mysql/ndb-operator/pkg/ndbconfig"
"github.com/mysql/ndb-operator/pkg/resources"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
listerscorev1 "k8s.io/client-go/listers/core/v1"
klog "k8s.io/klog/v2"
)
const (
mysqldClientName = constants.NdbNodeTypeMySQLD
// MySQL Server runtime directory
mysqldDir = constants.DataDir
// MySQL Cluster init script volume and mount path
mysqldInitScriptsVolName = mysqldClientName + "-init-scripts-vol"
mysqldInitScriptsMountPath = "/docker-entrypoint-initdb.d/"
// my.cnf configmap key, volume and mount path
mysqldCnfVolName = mysqldClientName + "-cnf-vol"
mysqldCnfMountPath = mysqldDir + "/cnf"
// LastAppliedMySQLServerConfigVersion is the annotation key that holds the last applied version of MySQL Server config (my.cnf version)
LastAppliedMySQLServerConfigVersion = ndbcontroller.GroupName + "/last-applied-my-cnf-config-version"
// RootPasswordSecret is the name of the secret that holds the password for the root account
RootPasswordSecret = ndbcontroller.GroupName + "/root-password-secret"
)
var (
// Ports to be exposed by the container and service
mysqldPorts = []int32{3306}
)
// mysqldStatefulSet implements the NdbStatefulSetInterface
// to control a set of MySQL Servers
type mysqldStatefulSet struct {
baseStatefulSet
configMapLister listerscorev1.ConfigMapLister
}
func (mss *mysqldStatefulSet) NewGoverningService(nc *v1.NdbCluster) *corev1.Service {
return newService(nc, mysqldPorts, mss.nodeType, false, nc.Spec.MysqlNode.EnableLoadBalancer)
}
// getPodVolumes returns the volumes to be used by the pod
func (mss *mysqldStatefulSet) getPodVolumes(ndb *v1.NdbCluster) ([]corev1.Volume, error) {
podVolumes := []corev1.Volume{
// Load the healthcheck script via a volume
{
Name: helperScriptsVolName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: ndb.GetConfigMapName(),
},
Items: []corev1.KeyToPath{
{
Key: constants.MysqldInitScript,
Path: constants.MysqldInitScript,
Mode: &ownerCanExecMode,
},
{
Key: constants.MysqldHealthCheckScript,
Path: constants.MysqldHealthCheckScript,
},
},
},
},
},
}
// Create a projected volume source to load all custom init scripts
var initScriptPvs corev1.ProjectedVolumeSource
// Create projections for all the scripts and append it to the initScriptPvs
for configMapName, configMapKeys := range ndb.Spec.MysqlNode.InitScripts {
cm, err := mss.configMapLister.ConfigMaps(ndb.Namespace).Get(configMapName)
if err != nil {
klog.Errorf("Failed to get configMap '%s/%s' : %s", ndb.Namespace, configMapName, err)
return nil, err
}
// Create a VolumeProjection with the configMap name
volProjection := corev1.VolumeProjection{
ConfigMap: &corev1.ConfigMapProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: configMapName,
},
},
}
appendKeyToPathFromConfigMapKey := func(configMapName, key string, projection *corev1.ConfigMapProjection) {
projection.Items = append(projection.Items, corev1.KeyToPath{
Key: key,
// The configmap name is prefixed to the key name to ensure
// that all scripts run in alphabetical order across config maps.
Path: configMapName + "_" + key + ".sql",
})
}
if len(configMapKeys) == 0 {
// Keys not mentioned - extract all keys
for key := range cm.Data {
appendKeyToPathFromConfigMapKey(configMapName, key, volProjection.ConfigMap)
}
} else {
// Keys from which the sql scripts have to be loaded are given.
for _, key := range configMapKeys {
appendKeyToPathFromConfigMapKey(configMapName, key, volProjection.ConfigMap)
}
}
initScriptPvs.Sources = append(initScriptPvs.Sources, volProjection)
}
// Append the custom init script volume to the podVolumes
podVolumes = append(podVolumes, corev1.Volume{
Name: mysqldInitScriptsVolName,
VolumeSource: corev1.VolumeSource{
Projected: &initScriptPvs,
},
})
if len(ndb.GetMySQLCnf()) > 0 {
// Load the cnf configmap key as a volume
podVolumes = append(podVolumes, corev1.Volume{
Name: mysqldCnfVolName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: ndb.GetConfigMapName(),
},
Items: []corev1.KeyToPath{
{
Key: constants.MySQLConfigKey,
Path: constants.MySQLConfigKey,
},
},
},
},
})
}
// An empty directory volume needs to be provided to the mysql server
// pods if the NdbCluster resource doesn't have any PVCs defined to
// be used with the mysql servers.
if ndb.Spec.MysqlNode.PVCSpec == nil {
podVolumes = append(podVolumes, *mss.getEmptyDirPodVolume(mss.getDataDirVolumeName()))
}
return podVolumes, nil
}
// getVolumeMounts returns pod volumes to be mounted into the container
func (mss *mysqldStatefulSet) getVolumeMounts(nc *v1.NdbCluster) []corev1.VolumeMount {
volumeMounts := []corev1.VolumeMount{
// Mount the data directory volume
{
Name: mss.getDataDirVolumeName(),
MountPath: dataDirectoryMountPath,
},
// Mount the init script volume
{
Name: mysqldInitScriptsVolName,
MountPath: mysqldInitScriptsMountPath,
},
// Volume mount for helper scripts
mss.getHelperScriptVolumeMount(),
// Mount the work dir volume
mss.getWorkDirVolumeMount(),
}
if len(nc.GetMySQLCnf()) > 0 {
// Mount the cnf volume
volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: mysqldCnfVolName,
MountPath: mysqldCnfMountPath,
})
}
return volumeMounts
}
// getMySQLServerCmd returns the command and arguments to start the MySQL Server
func (mss *mysqldStatefulSet) getMySQLServerCmd(nc *v1.NdbCluster) []string {
cmdAndArgs := []string{
"mysqld",
}
// Add the arguments to the command
// first, pass any provided cnf options via defaults-file
if len(nc.GetMySQLCnf()) > 0 {
cmdAndArgs = append(cmdAndArgs,
"--defaults-file="+mysqldCnfMountPath+"/"+constants.MySQLConfigKey)
}
// Add operator and NDB Cluster specific MySQL Server arguments
cmdAndArgs = append(cmdAndArgs,
// Enable ndbcluster engine and set connect string
"--ndbcluster",
"--ndb-connectstring="+nc.GetConnectstring(),
"--user=mysql",
"--datadir="+dataDirectoryMountPath,
"--ndb-cluster-connection-pool="+strconv.Itoa(int(nc.GetMySQLServerConnectionPoolSize())),
"--ndb-cluster-connection-pool-nodeids=$(cat "+NodeIdFilePath+")",
)
if debug.Enabled {
cmdAndArgs = append(cmdAndArgs,
// Enable maximum verbosity for development debugging
"--ndb-extra-logging=99",
"--log-error-verbosity=3",
)
}
return cmdAndArgs
}
// getInitDBContainer returns a new init container to initialize the data directory
func (mss *mysqldStatefulSet) getInitDBContainer(nc *v1.NdbCluster) corev1.Container {
// Generate the command and arguments to be used
cmdAndArgs := append([]string{
helperScriptsMountPath + "/" + constants.MysqldInitScript,
}, mss.getMySQLServerCmd(nc)...)
mysqlInitContainer := mss.createContainer(nc,
mss.getContainerName(true),
cmdAndArgs, mss.getVolumeMounts(nc), mysqldPorts)
// Add Env variables required by init script
ndbOperatorPodNamespace, _ := helpers.GetCurrentNamespace()
rootPasswordSecretName, _ := resources.GetMySQLRootPasswordSecretName(nc)
mysqlInitContainer.Env = append(mysqlInitContainer.Env, corev1.EnvVar{
// Password of the root user
Name: "MYSQL_ROOT_PASSWORD",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: rootPasswordSecretName,
},
Key: corev1.BasicAuthPasswordKey,
},
},
}, corev1.EnvVar{
// Host from which the ndb operator user account can be accessed.
// Use the hostname defined by the Ndb Operator deployment's template spec.
Name: "NDB_OPERATOR_HOST",
Value: "ndb-operator-pod.ndb-operator-svc." + ndbOperatorPodNamespace + ".svc.%",
}, corev1.EnvVar{
// Password of the NDB operator user
Name: "NDB_OPERATOR_PASSWORD",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: resources.GetMySQLNDBOperatorPasswordSecretName(nc),
},
Key: corev1.BasicAuthPasswordKey,
},
},
})
return mysqlInitContainer
}
// getContainers returns the containers to run a MySQL Server
func (mss *mysqldStatefulSet) getContainers(nc *v1.NdbCluster) []corev1.Container {
mysqldContainer := mss.createContainer(nc,
mss.getContainerName(false),
mss.getMySQLServerCmd(nc), mss.getVolumeMounts(nc), mysqldPorts)
// Create an exec handler that runs the MysqldHealthCheckScript to be used in health probes
healthProbeHandler := corev1.ProbeHandler{
Exec: &corev1.ExecAction{
Command: []string{
"/bin/bash",
helperScriptsMountPath + "/" + constants.MysqldHealthCheckScript,
dataDirectoryMountPath,
},
},
}
// Setup health probes.
// Startup probe - expects MySQL to get ready within 5 minutes
mysqldContainer.StartupProbe = &corev1.Probe{
ProbeHandler: healthProbeHandler,
PeriodSeconds: 2,
FailureThreshold: 150,
}
// Readiness probe
mysqldContainer.ReadinessProbe = &corev1.Probe{
ProbeHandler: healthProbeHandler,
}
return []corev1.Container{mysqldContainer}
}
func (mss *mysqldStatefulSet) getPodAntiAffinity() *corev1.PodAntiAffinity {
// Default pod AntiAffinity rules for Data Nodes
return GetPodAntiAffinityRules([]constants.NdbNodeType{
constants.NdbNodeTypeMgmd, constants.NdbNodeTypeNdbmtd, constants.NdbNodeTypeMySQLD,
})
}
// NewStatefulSet creates a new MySQL Server StatefulSet for the given NdbCluster.
func (mss *mysqldStatefulSet) NewStatefulSet(cs *ndbconfig.ConfigSummary, nc *v1.NdbCluster) (*appsv1.StatefulSet, error) {
statefulSet := mss.newStatefulSet(nc, cs)
statefulSetSpec := &statefulSet.Spec
// Fill in MySQL Server specific details
replicas := nc.GetMySQLServerNodeCount()
statefulSetSpec.Replicas = &replicas
// Set pod management policy to start MySQL Servers in parallel
statefulSetSpec.PodManagementPolicy = appsv1.ParallelPodManagement
// Update statefulset annotation
statefulSetAnnotations := statefulSet.GetAnnotations()
statefulSetAnnotations[RootPasswordSecret], _ = resources.GetMySQLRootPasswordSecretName(nc)
// Add VolumeClaimTemplate if data node PVC Spec exists
if nc.Spec.MysqlNode.PVCSpec != nil {
statefulSetSpec.VolumeClaimTemplates = []corev1.PersistentVolumeClaim{
// This PVC will be used as a template and an actual PVC will be created by the
// statefulset controller with name "<data-dir-vol-name(i.e mysqld-data-vol)>-<pod-name>"
*newPVC(nc, mss.getDataDirVolumeName(), nc.Spec.MysqlNode.PVCSpec),
}
}
// Update template pod spec
podSpec := &statefulSetSpec.Template.Spec
podSpec.InitContainers = append(podSpec.InitContainers, mss.getInitDBContainer(nc))
podSpec.Containers = mss.getContainers(nc)
podVolumes, err := mss.getPodVolumes(nc)
if err != nil {
klog.Errorf("Failed to get pod volumes for the statefulset %s", statefulSet.Name)
return nil, err
}
podSpec.Volumes = append(podSpec.Volumes, podVolumes...)
// Set default AntiAffinity rules
podSpec.Affinity = &corev1.Affinity{
PodAntiAffinity: mss.getPodAntiAffinity(),
}
// Copy down any podSpec specified via CRD
CopyPodSpecFromNdbPodSpec(podSpec, nc.Spec.MysqlNode.NdbPodSpec)
// Annotate the spec template with my.cnf version to trigger
// an update of MySQL Servers when my.cnf changes.
podAnnotations := statefulSetSpec.Template.GetAnnotations()
podAnnotations[LastAppliedMySQLServerConfigVersion] = strconv.FormatInt(int64(cs.MySQLServerConfigVersion), 10)
return statefulSet, nil
}
// NewMySQLdStatefulSet returns a new mysqldStatefulSet
func NewMySQLdStatefulSet(configMapLister listerscorev1.ConfigMapLister) NdbStatefulSetInterface {
return &mysqldStatefulSet{
baseStatefulSet{
nodeType: constants.NdbNodeTypeMySQLD,
},
configMapLister,
}
}