diff --git a/Jenkinsfile b/Jenkinsfile index 825ce4633..31d58d5e0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -260,6 +260,7 @@ void runTest(Integer TEST_ID) { export KUBECONFIG=/tmp/$CLUSTER_NAME-$clusterSuffix export PATH="\${KREW_ROOT:-\$HOME/.krew}/bin:\$PATH" export MYSQL_VERSION="$mysqlVer" + export MYSQL_UPSTREAM="true" set -o pipefail if [ -f ./e2e-tests/kuttl.yaml ]; then kubectl kuttl test --config ./e2e-tests/kuttl.yaml --test "^${testName}\$" |& tee e2e-tests/logs/${testNameWithMysqlVersion}.log diff --git a/api/v1/perconaservermysql_types.go b/api/v1/perconaservermysql_types.go index 7271ed02f..eee6e8fd8 100644 --- a/api/v1/perconaservermysql_types.go +++ b/api/v1/perconaservermysql_types.go @@ -524,6 +524,9 @@ func (b *BackupStorageAzureSpec) ContainerAndPrefix() (string, string) { type PiTRSpec struct { Enabled bool `json:"enabled,omitempty"` + // Image is the container image for the PITR restore Job (mysqlbinlog-based). When empty, spec.mysql.image is used. + Image string `json:"image,omitempty"` + BinlogServer *BinlogServerSpec `json:"binlogServer,omitempty"` } diff --git a/build/ps-entrypoint.sh b/build/ps-entrypoint.sh index 6b07998c7..576a1e8d5 100755 --- a/build/ps-entrypoint.sh +++ b/build/ps-entrypoint.sh @@ -140,6 +140,15 @@ CFG=/etc/my.cnf.d/node.cnf TLS_DIR=/etc/mysql/mysql-tls-secret CUSTOM_CONFIG_FILES=("/etc/mysql/config/auto-config.cnf" "/etc/mysql/config/my-config.cnf" "/etc/mysql/config/my-secret.cnf") +if [[ ${HOSTNAME} =~ "-xb-" ]]; then + FQDN=${HOSTNAME} +else + CLUSTER_NAME="${HOSTNAME%-[0-9]}" + SERVER_NUM=${HOSTNAME/$CLUSTER_NAME-/} + SERVER_ID=${CLUSTER_HASH}${SERVER_NUM} + FQDN="${HOSTNAME}.${SERVICE_NAME}.$(/var/lib/mysql/mysqld.my cp "${KEYRING_VAULT_PATH}" /var/lib/mysql/component_keyring_vault.cnf @@ -168,18 +177,11 @@ add_encryption_options() { } create_default_cnf() { - POD_IP=$(hostname -I | awk '{print $1}') - - if [[ ${HOSTNAME} =~ "-xb-" ]]; then - FQDN=${HOSTNAME} - else - CLUSTER_NAME="$(hostname -f | cut -d'.' -f2)" - SERVER_NUM=${HOSTNAME/$CLUSTER_NAME-/} - SERVER_ID=${CLUSTER_HASH}${SERVER_NUM} - FQDN="${HOSTNAME}.${SERVICE_NAME}.$($CFG + # Place the Unix socket in the data directory (not e.g. /var/run/mysqld/) so it shares volume permissions with mysqld. + sed -i "/\[mysqld\]/a socket=${DATADIR}/mysql.sock" $CFG + # Same for MySQL X Plugin’s Unix socket (otherwise defaults to /var/run/mysqld/mysqlx.sock). + sed -i "/\[mysqld\]/a mysqlx_socket=${DATADIR}/mysqlx.sock" $CFG if [[ ${CLUSTER_TYPE} == "async" ]]; then # Ensures replication is not automatically started on mysql startup, # giving the orchestrator full control over replication start. @@ -224,8 +226,6 @@ create_default_cnf() { } load_group_replication_plugin() { - POD_IP=$(hostname -I | awk '{print $1}') - sed -i "/\[mysqld\]/a plugin_load_add=group_replication.so" $CFG sed -i "/\[mysqld\]/a group_replication_exit_state_action=ABORT_SERVER" $CFG sed -i "/\[mysqld\]/a group_replication_unreachable_majority_timeout=5" $CFG @@ -236,6 +236,13 @@ ensure_read_only() { sed -i "/\[mysqld\]/a super_read_only=ON" $CFG } +# Sync node.cnf to /etc/mysql/conf.d so Oracle MySQL image picks it up (it uses !includedir /etc/mysql/conf.d/ only). +sync_node_cnf_for_oracle() { + if [ -d /etc/mysql/conf.d ]; then + cp -f "$CFG" /etc/mysql/conf.d/node.cnf + fi +} + escape_special() { { set +x; } 2>/dev/null echo "$1" \ @@ -285,7 +292,8 @@ if [ "$1" = 'mysqld' ] && [ -z "$wantHelp" ]; then rm -rfv "$TMPDIR" echo 'Database initialized' - SOCKET="$(_get_config 'socket' "$@")" + # Must match socket= in node.cnf (create_default_cnf); --verbose --help does not apply that file here. + SOCKET="${DATADIR}/mysql.sock" "$@" --skip-networking --socket="${SOCKET}" & pid="$!" @@ -442,6 +450,7 @@ if [ "$1" = 'mysqld' ] && [ -z "$wantHelp" ]; then load_group_replication_plugin ensure_read_only + sync_node_cnf_for_oracle # exit when MYSQL_INIT_ONLY environment variable is set to avoid starting mysqld if [ -n "$MYSQL_INIT_ONLY" ]; then @@ -462,7 +471,7 @@ fi if [[ -f /var/lib/mysql/full-cluster-crash ]]; then set +o xtrace - node_name=$(hostname -f) + node_name=${HOSTNAME} gtid_executed=$(@localhost" + echo "kubectl -n ${namespace} exec -it $FQDN -- mysqlsh root:@localhost" echo "and run the following command to reboot cluster:" echo "dba.rebootClusterFromCompleteOutage()" echo "and delete /var/lib/mysql/full-cluster-crash file in each pod." diff --git a/cmd/example-gen/pkg/defaults/manual.go b/cmd/example-gen/pkg/defaults/manual.go index d6f27b148..68fc66c2f 100644 --- a/cmd/example-gen/pkg/defaults/manual.go +++ b/cmd/example-gen/pkg/defaults/manual.go @@ -123,6 +123,7 @@ func backupDefaults(spec *apiv1.BackupSpec) { spec.Enabled = true spec.PiTR = apiv1.PiTRSpec{ Enabled: false, + Image: ImageMySQL, BinlogServer: &apiv1.BinlogServerSpec{ Storage: apiv1.BinlogServerStorageSpec{ S3: &apiv1.BackupStorageS3Spec{ diff --git a/cmd/example-gen/scripts/lib/ps.sh b/cmd/example-gen/scripts/lib/ps.sh index 2033ba3a0..5abae7608 100644 --- a/cmd/example-gen/scripts/lib/ps.sh +++ b/cmd/example-gen/scripts/lib/ps.sh @@ -15,7 +15,7 @@ sort_yaml() { ORCHESTRATOR_ORDER='"enabled", "expose", '"$POD_SPEC_ORDER" PMM_ORDER='"enabled","image","imagePullPolicy","serverHost","mysqlParams","containerSecurityContext", "resources", "readinessProbes", "livenessProbes"' - BINLOG_SERVER_ORDER='"enabled","binlogServer"' + BINLOG_SERVER_ORDER='"enabled","image","binlogServer"' BINLOG_SERVER_SPEC_ORDER='"size","image","imagePullPolicy","imagePullSecrets","serverId","storage","connectTimeout","readTimeout","writeTimeout","idleTime"' BACKUP_ORDER='"enabled","pitr","sourcePod","image","imagePullPolicy","imagePullSecrets","schedule","backoffLimit", "serviceAccountName", "initContainer", "containerSecurityContext", "resources","storages"' TOOLKIT_ORDER='"image","imagePullPolicy","imagePullSecrets","env","envFrom","resources","containerSecurityContext", "startupProbe", "readinessProbe", "livenessProbe"' @@ -210,6 +210,7 @@ del_fields_to_comment() { | yq "del(.spec.pmm.containerSecurityContext)" \ | yq "del(.spec.pmm.resources.limits)" \ | yq "del(.spec.backup.pitr.binlogServer)" \ + | yq "del(.spec.backup.pitr.image)" \ | yq "del(.spec.backup.sourcePod)" \ | yq "del(.spec.backup.schedule)" \ | yq "del(.spec.backup.backoffLimit)" \ diff --git a/config/crd/bases/ps.percona.com_perconaservermysqls.yaml b/config/crd/bases/ps.percona.com_perconaservermysqls.yaml index 96da62a74..27f64c204 100644 --- a/config/crd/bases/ps.percona.com_perconaservermysqls.yaml +++ b/config/crd/bases/ps.percona.com_perconaservermysqls.yaml @@ -1538,6 +1538,8 @@ spec: rule: '!has(self.size) || self.size <= 1' enabled: type: boolean + image: + type: string type: object x-kubernetes-validations: - message: binlogServer is required when pitr is enabled diff --git a/deploy/bundle.yaml b/deploy/bundle.yaml index a639ccfb7..329fb7f6f 100644 --- a/deploy/bundle.yaml +++ b/deploy/bundle.yaml @@ -3979,6 +3979,8 @@ spec: rule: '!has(self.size) || self.size <= 1' enabled: type: boolean + image: + type: string type: object x-kubernetes-validations: - message: binlogServer is required when pitr is enabled diff --git a/deploy/cr.yaml b/deploy/cr.yaml index ad6869b02..cc86dad1e 100644 --- a/deploy/cr.yaml +++ b/deploy/cr.yaml @@ -668,6 +668,7 @@ spec: enabled: true pitr: enabled: false +# image: perconalab/percona-server-mysql-operator:main-psmysql8.4 # binlogServer: # size: 1 # image: perconalab/percona-binlog-server:0.2.1 diff --git a/deploy/crd.yaml b/deploy/crd.yaml index 973a049fa..1f95309d6 100644 --- a/deploy/crd.yaml +++ b/deploy/crd.yaml @@ -3979,6 +3979,8 @@ spec: rule: '!has(self.size) || self.size <= 1' enabled: type: boolean + image: + type: string type: object x-kubernetes-validations: - message: binlogServer is required when pitr is enabled diff --git a/deploy/cw-bundle.yaml b/deploy/cw-bundle.yaml index 61e27eb86..aef064449 100644 --- a/deploy/cw-bundle.yaml +++ b/deploy/cw-bundle.yaml @@ -3979,6 +3979,8 @@ spec: rule: '!has(self.size) || self.size <= 1' enabled: type: boolean + image: + type: string type: object x-kubernetes-validations: - message: binlogServer is required when pitr is enabled diff --git a/e2e-tests/functions b/e2e-tests/functions index b0e1f008e..fa55564e4 100755 --- a/e2e-tests/functions +++ b/e2e-tests/functions @@ -703,6 +703,25 @@ get_storage_alias() { echo "$storage" } +# When MYSQL_UPSTREAM is set, add security settings required by the official mysql image (non-root uid/gid + fsGroup). +# spec.mysql is enough: backup/restore Jobs inherit these contexts unless a storage entry overrides them. +# Upstream mysql image lacks mysqlbinlog; PITR restore Job uses spec.backup.pitr.image (Percona Server main-psmysql* by default). +apply_mysql_upstream_cr_patch() { + if [[ -z ${MYSQL_UPSTREAM-} ]]; then + cat + return + fi + local cr_tmp + cr_tmp=$(mktemp) + cat >"${cr_tmp}" + IMAGE_PITR_RESTORE="${IMAGE_PITR_RESTORE}" yq eval ' + .spec.mysql.containerSecurityContext = {"runAsUser": 1001, "runAsGroup": 1001} | + .spec.mysql.podSecurityContext = {"fsGroup": 1001} | + .spec.backup.pitr.image = strenv(IMAGE_PITR_RESTORE) + ' "${cr_tmp}" + rm -f "${cr_tmp}" +} + get_test_cr() { local cr_name=${1:-$test_name} local platform=${2:-$(detect_k8s_platform)} @@ -768,13 +787,30 @@ get_cr() { | yq eval '.spec.proxy.haproxy.resources.requests.cpu = "300m"' - else cat - fi + fi \ + | apply_mysql_upstream_cr_patch } get_client_pod() { - kubectl -n "${NAMESPACE}" get pods \ - --selector=name=mysql-client \ - -o 'jsonpath={.items[].metadata.name}' + # Use {.items[*].metadata.name}: {.items[].…} errors with "array index out of bounds" + # when there are zero pods (recent kubectl). Retry briefly so deploy_client can finish. + local pod="" + local retry=0 + while [[ $retry -lt 120 ]]; do + pod=$(kubectl -n "${NAMESPACE}" get pods \ + --selector=name=mysql-client \ + -o 'jsonpath={.items[*].metadata.name}' 2>/dev/null) || true + pod="${pod%% *}" + if [[ -n "$pod" ]]; then + echo "$pod" + return 0 + fi + sleep 1 + retry=$((retry + 1)) + done + echo "ERROR: mysql-client pod not found after 120s (namespace=${NAMESPACE}). Ensure deploy_client ran (e.g. 00-deploy-operator)." >&2 + kubectl -n "${NAMESPACE}" get pods -l name=mysql-client -o wide >&2 || true + return 1 } run_mysql() { @@ -786,8 +822,31 @@ run_mysql() { client_pod=$(get_client_pod) wait_pod $client_pod 1>&2 - kubectl -n "${NAMESPACE}" exec "${pod:-mysql-client}" -- \ - bash -c "printf '%s\n' \"${command}\" | mysql -sN $host $user" 2>&1 \ + local target_pod="${pod:-mysql-client}" + local container_opt=() + if [[ "${target_pod}" != "mysql-client" ]]; then + container_opt=(-c mysql) + fi + + # 'user'@'localhost' requires a Unix socket; -h localhost uses the *client* default, which often + # does not match mysqld (e.g. /var/run/mysqld/mysqld.sock vs /var/lib/mysql/mysql.sock). + # Operator + ps-entrypoint always use ${DataMountPath}/mysql.sock (see pkg/mysql DataMountPath). + local conn_args="${host}" + if [[ "${target_pod}" != "mysql-client" && "${host}" == *"localhost"* ]]; then + local root_pw socket operator_sock="/var/lib/mysql/mysql.sock" + root_pw=$(get_user_pass root) + # Root must use the same socket as the server; a bare mysql -uroot would repeat the client-default problem. + socket=$(kubectl -n "${NAMESPACE}" exec "${target_pod}" "${container_opt[@]}" -- \ + env MYSQL_PWD="${root_pw}" mysql -sN -uroot --socket="${operator_sock}" -e "SELECT @@socket" 2>/dev/null | tail -1) + socket=$(echo "${socket}" | tr -d '\r' | tr -d '[:space:]') + if [[ -z "${socket}" ]]; then + socket="${operator_sock}" + fi + conn_args="--socket=${socket}" + fi + + kubectl -n "${NAMESPACE}" exec "${target_pod}" "${container_opt[@]}" -- \ + bash -c "printf '%s\n' \"${command}\" | mysql -sN ${conn_args} ${user}" 2>&1 \ | $sed -e 's/mysql: //' \ | (grep -v 'Using a password on the command line interface can be insecure.' || :) } @@ -797,12 +856,18 @@ run_mysqlsh() { local host="$2" local user="${3:--uroot -p'$(get_user_pass)'}" local pod="$4" + local sql_mode="--sql" + + # Port 33060 is MySQL X Protocol; --sql uses classic protocol and fails on 8.4+ (ERROR 2007). + if [[ "$host" =~ -P[[:space:]]*33060 ]] || [[ "$host" =~ :33060 ]]; then + sql_mode="--sqlx" + fi client_pod=$(get_client_pod) wait_pod $client_pod 1>&2 kubectl -n "${NAMESPACE}" exec "${pod:-mysql-client}" -- \ - bash -c "printf '%s\n' \"${command}\" | mysqlsh --sql --quiet-start=2 $host $user" 2>/dev/null \ + bash -c "printf '%s\n' \"${command}\" | mysqlsh ${sql_mode} --quiet-start=2 $host $user" 2>/dev/null \ | tail -n +2 } diff --git a/e2e-tests/tests/auto-config/01-assert.yaml b/e2e-tests/tests/auto-config/01-assert.yaml index af7d723ff..84fed41ad 100644 --- a/e2e-tests/tests/auto-config/01-assert.yaml +++ b/e2e-tests/tests/auto-config/01-assert.yaml @@ -25,6 +25,10 @@ spec: name: tls - mountPath: /etc/mysql/config name: config + - mountPath: /etc/my.cnf.d + name: node-cnf + - mountPath: /etc/mysql/conf.d + name: node-cnf-conf-d - mountPath: /etc/mysql/vault-keyring-secret name: vault-keyring-secret - name: xtrabackup @@ -86,6 +90,10 @@ spec: optional: true - emptyDir: {} name: backup-logs + - emptyDir: {} + name: node-cnf + - emptyDir: {} + name: node-cnf-conf-d - name: vault-keyring-secret secret: defaultMode: 420 diff --git a/e2e-tests/tests/config-router/01-assert.yaml b/e2e-tests/tests/config-router/01-assert.yaml index a6f0a0707..a37c5b248 100644 --- a/e2e-tests/tests/config-router/01-assert.yaml +++ b/e2e-tests/tests/config-router/01-assert.yaml @@ -25,6 +25,10 @@ spec: name: tls - mountPath: /etc/mysql/config name: config + - mountPath: /etc/my.cnf.d + name: node-cnf + - mountPath: /etc/mysql/conf.d + name: node-cnf-conf-d - mountPath: /etc/mysql/vault-keyring-secret name: vault-keyring-secret - name: xtrabackup @@ -78,6 +82,10 @@ spec: optional: true - emptyDir: {} name: backup-logs + - emptyDir: {} + name: node-cnf + - emptyDir: {} + name: node-cnf-conf-d - name: vault-keyring-secret secret: defaultMode: 420 diff --git a/e2e-tests/tests/config/01-assert.yaml b/e2e-tests/tests/config/01-assert.yaml index b1baf1963..a674185fa 100644 --- a/e2e-tests/tests/config/01-assert.yaml +++ b/e2e-tests/tests/config/01-assert.yaml @@ -25,6 +25,10 @@ spec: name: tls - mountPath: /etc/mysql/config name: config + - mountPath: /etc/my.cnf.d + name: node-cnf + - mountPath: /etc/mysql/conf.d + name: node-cnf-conf-d - mountPath: /etc/mysql/vault-keyring-secret name: vault-keyring-secret - name: xtrabackup @@ -86,6 +90,10 @@ spec: optional: true - emptyDir: {} name: backup-logs + - emptyDir: {} + name: node-cnf + - emptyDir: {} + name: node-cnf-conf-d - name: vault-keyring-secret secret: defaultMode: 420 diff --git a/e2e-tests/tests/config/09-check-retry-vars.yaml b/e2e-tests/tests/config/09-check-retry-vars.yaml index 3bcd32e14..ecbe7ec26 100644 --- a/e2e-tests/tests/config/09-check-retry-vars.yaml +++ b/e2e-tests/tests/config/09-check-retry-vars.yaml @@ -9,7 +9,7 @@ commands: t=1 for pod in $(kubectl get pods -n "${NAMESPACE}" -l 'app.kubernetes.io/component=database,!mysql.percona.com/primary' -o jsonpath='{.items[*].metadata.name}'); do - connect_retry=$(kubectl exec $pod -n "${NAMESPACE}" -- bash -c 'mysql -uroot -p"$(cat /etc/mysql/mysql-users-secret/root)" -e "show replica status\G;"' | egrep "Connect_Retry|Source_Retry_Count") + connect_retry=$(kubectl exec $pod -n "${NAMESPACE}" -- bash -c 'mysql -uroot -p"$(cat /etc/mysql/mysql-users-secret/root)" -h127.0.0.1 -P3306 --protocol=tcp -e "show replica status\G;"' | egrep "Connect_Retry|Source_Retry_Count") echo "Connect retry for $pod: $connect_retry" kubectl create cm 09-check-retry-vars-$t -n "${NAMESPACE}" --from-literal=retry_values="$connect_retry" t=$((t+1)) diff --git a/e2e-tests/tests/limits/01-assert.yaml b/e2e-tests/tests/limits/01-assert.yaml index fd4c91f56..29951052f 100644 --- a/e2e-tests/tests/limits/01-assert.yaml +++ b/e2e-tests/tests/limits/01-assert.yaml @@ -72,6 +72,11 @@ spec: value: /var/lib/mysql/notify.sock - name: MYSQL_STATE_FILE value: /var/lib/mysql/mysql.state + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP - name: KEYRING_VAULT_PATH value: /etc/mysql/vault-keyring-secret/keyring_vault.cnf - name: BACKUPS_ENABLED @@ -135,6 +140,10 @@ spec: name: tls - mountPath: /etc/mysql/config name: config + - mountPath: /etc/my.cnf.d + name: node-cnf + - mountPath: /etc/mysql/conf.d + name: node-cnf-conf-d - mountPath: /etc/mysql/vault-keyring-secret name: vault-keyring-secret - command: @@ -181,7 +190,9 @@ spec: - /opt/percona-server-mysql-operator/ps-init-entrypoint.sh imagePullPolicy: Always name: mysql-init - resources: {} + resources: + requests: + memory: 2Gi terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -189,9 +200,6 @@ spec: name: bin restartPolicy: Always schedulerName: default-scheduler - securityContext: - supplementalGroups: - - 1001 terminationGracePeriodSeconds: 30 volumes: - emptyDir: {} @@ -230,6 +238,10 @@ spec: optional: true - emptyDir: {} name: backup-logs + - emptyDir: {} + name: node-cnf + - emptyDir: {} + name: node-cnf-conf-d - name: vault-keyring-secret secret: defaultMode: 420 @@ -399,9 +411,6 @@ spec: name: bin restartPolicy: Always schedulerName: default-scheduler - securityContext: - supplementalGroups: - - 1001 serviceAccount: percona-server-mysql-operator-orchestrator serviceAccountName: percona-server-mysql-operator-orchestrator terminationGracePeriodSeconds: 30 diff --git a/e2e-tests/tests/limits/03-assert.yaml b/e2e-tests/tests/limits/03-assert.yaml index 34fd8a50f..c51b72e16 100644 --- a/e2e-tests/tests/limits/03-assert.yaml +++ b/e2e-tests/tests/limits/03-assert.yaml @@ -72,6 +72,11 @@ spec: value: /var/lib/mysql/notify.sock - name: MYSQL_STATE_FILE value: /var/lib/mysql/mysql.state + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP - name: KEYRING_VAULT_PATH value: /etc/mysql/vault-keyring-secret/keyring_vault.cnf - name: BACKUPS_ENABLED @@ -135,6 +140,10 @@ spec: name: tls - mountPath: /etc/mysql/config name: config + - mountPath: /etc/my.cnf.d + name: node-cnf + - mountPath: /etc/mysql/conf.d + name: node-cnf-conf-d - mountPath: /etc/mysql/vault-keyring-secret name: vault-keyring-secret - command: @@ -181,7 +190,9 @@ spec: - /opt/percona-server-mysql-operator/ps-init-entrypoint.sh imagePullPolicy: Always name: mysql-init - resources: {} + resources: + limits: + memory: 4Gi terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: @@ -189,9 +200,6 @@ spec: name: bin restartPolicy: Always schedulerName: default-scheduler - securityContext: - supplementalGroups: - - 1001 terminationGracePeriodSeconds: 30 volumes: - emptyDir: {} @@ -230,6 +238,10 @@ spec: optional: true - emptyDir: {} name: backup-logs + - emptyDir: {} + name: node-cnf + - emptyDir: {} + name: node-cnf-conf-d - name: vault-keyring-secret secret: defaultMode: 420 @@ -399,9 +411,6 @@ spec: name: bin restartPolicy: Always schedulerName: default-scheduler - securityContext: - supplementalGroups: - - 1001 serviceAccount: percona-server-mysql-operator-orchestrator serviceAccountName: percona-server-mysql-operator-orchestrator terminationGracePeriodSeconds: 30 diff --git a/e2e-tests/tests/limits/05-assert.yaml b/e2e-tests/tests/limits/05-assert.yaml index df9559439..c925f572d 100644 --- a/e2e-tests/tests/limits/05-assert.yaml +++ b/e2e-tests/tests/limits/05-assert.yaml @@ -72,6 +72,11 @@ spec: value: /var/lib/mysql/notify.sock - name: MYSQL_STATE_FILE value: /var/lib/mysql/mysql.state + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP - name: KEYRING_VAULT_PATH value: /etc/mysql/vault-keyring-secret/keyring_vault.cnf - name: BACKUPS_ENABLED @@ -133,6 +138,10 @@ spec: name: tls - mountPath: /etc/mysql/config name: config + - mountPath: /etc/my.cnf.d + name: node-cnf + - mountPath: /etc/mysql/conf.d + name: node-cnf-conf-d - mountPath: /etc/mysql/vault-keyring-secret name: vault-keyring-secret - command: @@ -187,9 +196,6 @@ spec: name: bin restartPolicy: Always schedulerName: default-scheduler - securityContext: - supplementalGroups: - - 1001 terminationGracePeriodSeconds: 30 volumes: - emptyDir: {} @@ -228,6 +234,10 @@ spec: optional: true - emptyDir: {} name: backup-logs + - emptyDir: {} + name: node-cnf + - emptyDir: {} + name: node-cnf-conf-d - name: vault-keyring-secret secret: defaultMode: 420 @@ -395,9 +405,6 @@ spec: name: bin restartPolicy: Always schedulerName: default-scheduler - securityContext: - supplementalGroups: - - 1001 serviceAccount: percona-server-mysql-operator-orchestrator serviceAccountName: percona-server-mysql-operator-orchestrator terminationGracePeriodSeconds: 30 diff --git a/e2e-tests/tests/sidecars/01-assert.yaml b/e2e-tests/tests/sidecars/01-assert.yaml index 76a6bb709..88ef78146 100644 --- a/e2e-tests/tests/sidecars/01-assert.yaml +++ b/e2e-tests/tests/sidecars/01-assert.yaml @@ -27,6 +27,10 @@ spec: name: tls - mountPath: /etc/mysql/config name: config + - mountPath: /etc/my.cnf.d + name: node-cnf + - mountPath: /etc/mysql/conf.d + name: node-cnf-conf-d - mountPath: /etc/mysql/vault-keyring-secret name: vault-keyring-secret - name: xtrabackup @@ -109,6 +113,10 @@ spec: optional: true - emptyDir: {} name: backup-logs + - emptyDir: {} + name: node-cnf + - emptyDir: {} + name: node-cnf-conf-d - name: vault-keyring-secret secret: defaultMode: 420 diff --git a/e2e-tests/vars.sh b/e2e-tests/vars.sh index 3e18e19e2..4c2d8857a 100755 --- a/e2e-tests/vars.sh +++ b/e2e-tests/vars.sh @@ -17,6 +17,14 @@ if [[ -z ${MYSQL_VERSION-} && -n ${IMAGE_MYSQL-} ]]; then else export MYSQL_VERSION=${MYSQL_VERSION:-"8.4"} fi + +# Upstream Oracle mysql image in e2e: apply_mysql_upstream_cr_patch sets spec.mysql security and spec.backup.pitr.image +# to Percona Server (main-psmysql*) so mysqlbinlog is available; override with IMAGE_PITR_RESTORE if needed. +if [[ -n ${MYSQL_UPSTREAM-} ]]; then + export IMAGE_MYSQL="${IMAGE_MYSQL:-mysql:${MYSQL_VERSION}}" + export IMAGE_PITR_RESTORE="${IMAGE_PITR_RESTORE:-perconalab/percona-server-mysql-operator:main-psmysql${MYSQL_VERSION}}" +fi + export IMAGE_MYSQL=${IMAGE_MYSQL:-"perconalab/percona-server-mysql-operator:main-psmysql${MYSQL_VERSION}"} export IMAGE_BACKUP=${IMAGE_BACKUP:-"perconalab/percona-server-mysql-operator:main-backup${MYSQL_VERSION}"} export IMAGE_ORCHESTRATOR=${IMAGE_ORCHESTRATOR:-"perconalab/percona-server-mysql-operator:main-orchestrator"} diff --git a/pkg/k8s/utils.go b/pkg/k8s/utils.go index d3f59463a..d194e2a11 100644 --- a/pkg/k8s/utils.go +++ b/pkg/k8s/utils.go @@ -596,3 +596,23 @@ func EqualMetadata(m ...metav1.ObjectMeta) bool { } return true } + +// EffectiveBackupStoragePodSecurityContext returns the pod SecurityContext for backup-related Jobs +// (backup, delete, physical restore, PITR restore). Defaults to spec.mysql.podSecurityContext; per-storage +// podSecurityContext overrides when set. +func EffectiveBackupStoragePodSecurityContext(cluster *apiv1.PerconaServerMySQL, storage *apiv1.BackupStorageSpec) *corev1.PodSecurityContext { + if storage != nil && storage.PodSecurityContext != nil { + return storage.PodSecurityContext + } + return cluster.MySQLSpec().PodSecurityContext +} + +// EffectiveBackupStorageContainerSecurityContext returns the container SecurityContext for the same Jobs as +// EffectiveBackupStoragePodSecurityContext. Defaults to spec.mysql.containerSecurityContext; per-storage +// containerSecurityContext overrides when set. +func EffectiveBackupStorageContainerSecurityContext(cluster *apiv1.PerconaServerMySQL, storage *apiv1.BackupStorageSpec) *corev1.SecurityContext { + if storage != nil && storage.ContainerSecurityContext != nil { + return storage.ContainerSecurityContext + } + return cluster.MySQLSpec().ContainerSecurityContext +} diff --git a/pkg/mysql/mysql.go b/pkg/mysql/mysql.go index 4ed42a37c..a4f308d5a 100644 --- a/pkg/mysql/mysql.go +++ b/pkg/mysql/mysql.go @@ -26,6 +26,10 @@ const ( CustomConfigKey = "my.cnf" configVolumeName = "config" configMountPath = "/etc/mysql/config" + nodeCnfVolumeName = "node-cnf" + nodeCnfMountPath = "/etc/my.cnf.d" + nodeCnfConfDVolumeName = "node-cnf-conf-d" // Oracle MySQL image uses !includedir /etc/mysql/conf.d/ + nodeCnfConfDMountPath = "/etc/mysql/conf.d" CustomMyCnfPath = "/etc/mysql/config/my-config.cnf" credsVolumeName = "users" mysqlshVolumeName = "mysqlsh" @@ -313,6 +317,23 @@ func volumes(cr *apiv1.PerconaServerMySQL) []corev1.Volume { }, } + if cr.CompareVersion("1.2.0") >= 0 { + volumes = append(volumes, + corev1.Volume{ + Name: nodeCnfVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + corev1.Volume{ + Name: nodeCnfConfDVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + ) + } + if cr.CompareVersion("0.11.0") >= 0 { volumes = append(volumes, corev1.Volume{ Name: vaultSecretVolumeName, @@ -620,6 +641,19 @@ func mysqldVolumeMounts(cr *apiv1.PerconaServerMySQL) []corev1.VolumeMount { }, } + if cr.CompareVersion("1.2.0") >= 0 { + mounts = append(mounts, + corev1.VolumeMount{ + Name: nodeCnfVolumeName, + MountPath: nodeCnfMountPath, + }, + corev1.VolumeMount{ + Name: nodeCnfConfDVolumeName, + MountPath: nodeCnfConfDMountPath, + }, + ) + } + if cr.CompareVersion("0.11.0") >= 0 { mounts = append(mounts, corev1.VolumeMount{ Name: vaultSecretVolumeName, @@ -673,6 +707,17 @@ func mysqldContainer(cr *apiv1.PerconaServerMySQL) corev1.Container { } env = append(env, spec.Env...) + if cr.CompareVersion("1.2.0") >= 0 { + env = append(env, corev1.EnvVar{ + Name: naming.EnvPodIP, + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIP", + }, + }, + }) + } + if cr.CompareVersion("0.12.0") >= 0 { env = append(env, corev1.EnvVar{ Name: "KEYRING_VAULT_PATH", diff --git a/pkg/mysql/mysql_test.go b/pkg/mysql/mysql_test.go index 19bdb8c9b..d8074892e 100644 --- a/pkg/mysql/mysql_test.go +++ b/pkg/mysql/mysql_test.go @@ -509,6 +509,18 @@ func expectedVolumes() []corev1.Volume { EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, + { + Name: "node-cnf", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: "node-cnf-conf-d", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, { Name: "vault-keyring-secret", VolumeSource: corev1.VolumeSource{ diff --git a/pkg/naming/env.go b/pkg/naming/env.go index f0d15a687..cf4f8dea1 100644 --- a/pkg/naming/env.go +++ b/pkg/naming/env.go @@ -12,6 +12,8 @@ const ( EnvBootstrapCloneTimeout = "BOOTSTRAP_CLONE_TIMEOUT" + EnvPodIP = "POD_IP" + EnvAsyncSourceRetryCount = "ASYNC_SOURCE_RETRY_COUNT" EnvAsyncSourceConnectRetry = "ASYNC_SOURCE_CONNECT_RETRY" diff --git a/pkg/pitr/pitr.go b/pkg/pitr/pitr.go index e1f766f53..1340311fa 100644 --- a/pkg/pitr/pitr.go +++ b/pkg/pitr/pitr.go @@ -89,7 +89,7 @@ func RestoreJob( initImage, cluster.Spec.Backup.InitContainer, cluster.Spec.Backup.ImagePullPolicy, - storage.ContainerSecurityContext, + k8s.EffectiveBackupStorageContainerSecurityContext(cluster, storage), cluster.Spec.Backup.Resources, []corev1.VolumeMount{ { @@ -118,7 +118,7 @@ func RestoreJob( PriorityClassName: storage.PriorityClassName, RuntimeClassName: storage.RuntimeClassName, DNSPolicy: corev1.DNSClusterFirst, - SecurityContext: storage.PodSecurityContext, + SecurityContext: k8s.EffectiveBackupStoragePodSecurityContext(cluster, storage), Volumes: []corev1.Volume{ { Name: apiv1.BinVolumeName, @@ -168,12 +168,28 @@ func RestoreJob( } } +func pitrRestoreImage(cluster *apiv1.PerconaServerMySQL) (string, corev1.PullPolicy) { + mysqlSpec := cluster.Spec.MySQL + backup := cluster.Spec.Backup + if backup != nil { + if img := backup.PiTR.Image; img != "" { + policy := backup.ImagePullPolicy + if policy == "" { + policy = mysqlSpec.ImagePullPolicy + } + return img, policy + } + } + return mysqlSpec.Image, mysqlSpec.ImagePullPolicy +} + func restoreContainer( cluster *apiv1.PerconaServerMySQL, restore *apiv1.PerconaServerMySQLRestore, storage *apiv1.BackupStorageSpec, ) corev1.Container { binlogServer := cluster.Spec.Backup.PiTR.BinlogServer + img, pullPolicy := pitrRestoreImage(cluster) envs := []corev1.EnvVar{ { @@ -257,8 +273,8 @@ func restoreContainer( return corev1.Container{ Name: appName, - Image: cluster.Spec.MySQL.Image, - ImagePullPolicy: cluster.Spec.MySQL.ImagePullPolicy, + Image: img, + ImagePullPolicy: pullPolicy, Env: envs, VolumeMounts: []corev1.VolumeMount{ { @@ -285,7 +301,7 @@ func restoreContainer( Command: []string{"/opt/percona/run-pitr-restore.sh"}, TerminationMessagePath: "/dev/termination-log", TerminationMessagePolicy: corev1.TerminationMessageReadFile, - SecurityContext: storage.ContainerSecurityContext, + SecurityContext: k8s.EffectiveBackupStorageContainerSecurityContext(cluster, storage), Resources: storage.Resources, } } diff --git a/pkg/pitr/pitr_test.go b/pkg/pitr/pitr_test.go index a264e1f18..4113ba936 100644 --- a/pkg/pitr/pitr_test.go +++ b/pkg/pitr/pitr_test.go @@ -586,6 +586,170 @@ func TestRestoreJob(t *testing.T) { assert.Len(t, job.Spec.Template.Spec.InitContainers, 1) }, }, + "pitr restore container uses mysql image by default": { + cluster: &apiv1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "ns"}, + Spec: apiv1.PerconaServerMySQLSpec{ + SecretsName: "secrets", + SSLSecretName: "ssl", + MySQL: apiv1.MySQLSpec{ + PodSpec: apiv1.PodSpec{ + ContainerSpec: apiv1.ContainerSpec{ + Image: "mysql:8.4", + ImagePullPolicy: corev1.PullAlways, + }, + }, + }, + Backup: &apiv1.BackupSpec{ + PiTR: apiv1.PiTRSpec{ + BinlogServer: &apiv1.BinlogServerSpec{}, + }, + }, + }, + }, + restore: &apiv1.PerconaServerMySQLRestore{ + ObjectMeta: metav1.ObjectMeta{Name: "restore", Namespace: "ns"}, + }, + storage: &apiv1.BackupStorageSpec{}, + initImage: "init:latest", + verify: func(t *testing.T, job *batchv1.Job) { + c := job.Spec.Template.Spec.Containers[0] + assert.Equal(t, "mysql:8.4", c.Image) + assert.Equal(t, corev1.PullAlways, c.ImagePullPolicy) + }, + }, + "pitr restore container uses spec.backup.pitr.image when set": { + cluster: &apiv1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "ns"}, + Spec: apiv1.PerconaServerMySQLSpec{ + SecretsName: "secrets", + SSLSecretName: "ssl", + MySQL: apiv1.MySQLSpec{ + PodSpec: apiv1.PodSpec{ + ContainerSpec: apiv1.ContainerSpec{ + Image: "mysql:8.4", + ImagePullPolicy: corev1.PullNever, + }, + }, + }, + Backup: &apiv1.BackupSpec{ + Image: "backup:tool", + ImagePullPolicy: corev1.PullIfNotPresent, + PiTR: apiv1.PiTRSpec{ + Image: "pitr-restore:custom", + BinlogServer: &apiv1.BinlogServerSpec{}, + }, + }, + }, + }, + restore: &apiv1.PerconaServerMySQLRestore{ + ObjectMeta: metav1.ObjectMeta{Name: "restore", Namespace: "ns"}, + }, + storage: &apiv1.BackupStorageSpec{}, + initImage: "init:latest", + verify: func(t *testing.T, job *batchv1.Job) { + c := job.Spec.Template.Spec.Containers[0] + assert.Equal(t, "pitr-restore:custom", c.Image) + assert.Equal(t, corev1.PullIfNotPresent, c.ImagePullPolicy) + }, + }, + "pitr restore image falls back to mysql pull policy when backup policy unset": { + cluster: &apiv1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "ns"}, + Spec: apiv1.PerconaServerMySQLSpec{ + SecretsName: "secrets", + SSLSecretName: "ssl", + MySQL: apiv1.MySQLSpec{ + PodSpec: apiv1.PodSpec{ + ContainerSpec: apiv1.ContainerSpec{ + Image: "mysql:8.4", + ImagePullPolicy: corev1.PullNever, + }, + }, + }, + Backup: &apiv1.BackupSpec{ + PiTR: apiv1.PiTRSpec{ + Image: "pitr-restore:only-image", + BinlogServer: &apiv1.BinlogServerSpec{}, + }, + }, + }, + }, + restore: &apiv1.PerconaServerMySQLRestore{ + ObjectMeta: metav1.ObjectMeta{Name: "restore", Namespace: "ns"}, + }, + storage: &apiv1.BackupStorageSpec{}, + initImage: "init:latest", + verify: func(t *testing.T, job *batchv1.Job) { + c := job.Spec.Template.Spec.Containers[0] + assert.Equal(t, "pitr-restore:only-image", c.Image) + assert.Equal(t, corev1.PullNever, c.ImagePullPolicy) + }, + }, + "container security context from mysql": { + cluster: &apiv1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "ns"}, + Spec: apiv1.PerconaServerMySQLSpec{ + SecretsName: "secrets", + SSLSecretName: "ssl", + MySQL: apiv1.MySQLSpec{ + PodSpec: apiv1.PodSpec{ + ContainerSpec: apiv1.ContainerSpec{ + ContainerSecurityContext: &corev1.SecurityContext{RunAsUser: ptr.To(int64(3003))}, + }, + }, + }, + Backup: &apiv1.BackupSpec{ + PiTR: apiv1.PiTRSpec{ + BinlogServer: &apiv1.BinlogServerSpec{}, + }, + }, + }, + }, + restore: &apiv1.PerconaServerMySQLRestore{ + ObjectMeta: metav1.ObjectMeta{Name: "restore", Namespace: "ns"}, + }, + storage: &apiv1.BackupStorageSpec{}, + initImage: "init:latest", + verify: func(t *testing.T, job *batchv1.Job) { + want := &corev1.SecurityContext{RunAsUser: ptr.To(int64(3003))} + assert.Equal(t, want, job.Spec.Template.Spec.InitContainers[0].SecurityContext) + assert.Equal(t, want, job.Spec.Template.Spec.Containers[0].SecurityContext) + }, + }, + "storage container security context overrides mysql": { + cluster: &apiv1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "ns"}, + Spec: apiv1.PerconaServerMySQLSpec{ + SecretsName: "secrets", + SSLSecretName: "ssl", + MySQL: apiv1.MySQLSpec{ + PodSpec: apiv1.PodSpec{ + ContainerSpec: apiv1.ContainerSpec{ + ContainerSecurityContext: &corev1.SecurityContext{RunAsUser: ptr.To(int64(3003))}, + }, + }, + }, + Backup: &apiv1.BackupSpec{ + PiTR: apiv1.PiTRSpec{ + BinlogServer: &apiv1.BinlogServerSpec{}, + }, + }, + }, + }, + restore: &apiv1.PerconaServerMySQLRestore{ + ObjectMeta: metav1.ObjectMeta{Name: "restore", Namespace: "ns"}, + }, + storage: &apiv1.BackupStorageSpec{ + ContainerSecurityContext: &corev1.SecurityContext{RunAsUser: ptr.To(int64(4004))}, + }, + initImage: "init:latest", + verify: func(t *testing.T, job *batchv1.Job) { + want := &corev1.SecurityContext{RunAsUser: ptr.To(int64(4004))} + assert.Equal(t, want, job.Spec.Template.Spec.InitContainers[0].SecurityContext) + assert.Equal(t, want, job.Spec.Template.Spec.Containers[0].SecurityContext) + }, + }, } for name, tt := range tests { diff --git a/pkg/xtrabackup/xtrabackup.go b/pkg/xtrabackup/xtrabackup.go index 6276a3032..93ca7e92f 100644 --- a/pkg/xtrabackup/xtrabackup.go +++ b/pkg/xtrabackup/xtrabackup.go @@ -155,7 +155,7 @@ func Job( initImage, cluster.Spec.Backup.InitContainer, cluster.Spec.Backup.ImagePullPolicy, - storage.ContainerSecurityContext, + k8s.EffectiveBackupStorageContainerSecurityContext(cluster, storage), cluster.Spec.Backup.Resources, nil, ), @@ -164,7 +164,7 @@ func Job( xbContainer, }, ImagePullSecrets: cluster.Spec.Backup.ImagePullSecrets, - SecurityContext: storage.PodSecurityContext, + SecurityContext: k8s.EffectiveBackupStoragePodSecurityContext(cluster, storage), Affinity: storage.Affinity, TopologySpreadConstraints: storage.TopologySpreadConstraints, Tolerations: storage.Tolerations, @@ -261,7 +261,7 @@ func xtrabackupContainer(cluster *apiv1.PerconaServerMySQL, cr *apiv1.PerconaSer Command: []string{"/opt/percona/run-backup.sh"}, TerminationMessagePath: "/dev/termination-log", TerminationMessagePolicy: corev1.TerminationMessageReadFile, - SecurityContext: storage.ContainerSecurityContext, + SecurityContext: k8s.EffectiveBackupStorageContainerSecurityContext(cluster, storage), Resources: storage.Resources, }, nil } @@ -360,7 +360,7 @@ func (conf *BackupConfig) XbcloudGetArgs(files ...string) []string { return args } -func deleteContainer(image string, conf *BackupConfig, cr *apiv1.PerconaServerMySQLBackup, storage *apiv1.BackupStorageSpec) corev1.Container { +func deleteContainer(image string, conf *BackupConfig, cluster *apiv1.PerconaServerMySQL, cr *apiv1.PerconaServerMySQLBackup, storage *apiv1.BackupStorageSpec) corev1.Container { return corev1.Container{ Name: appName, Image: image, @@ -375,7 +375,7 @@ func deleteContainer(image string, conf *BackupConfig, cr *apiv1.PerconaServerMy Command: append([]string{"xbcloud"}, conf.XbcloudDeleteArgs()...), TerminationMessagePath: "/dev/termination-log", TerminationMessagePolicy: corev1.TerminationMessageReadFile, - SecurityContext: storage.ContainerSecurityContext, + SecurityContext: k8s.EffectiveBackupStorageContainerSecurityContext(cluster, storage), Resources: storage.Resources, } } @@ -421,7 +421,7 @@ func RestoreJob( k8s.InitContainer(cluster, appName, initImage, cluster.Spec.Backup.InitContainer, cluster.Spec.Backup.ImagePullPolicy, - storage.ContainerSecurityContext, + k8s.EffectiveBackupStorageContainerSecurityContext(cluster, storage), cluster.Spec.Backup.Resources, []corev1.VolumeMount{ { @@ -449,7 +449,7 @@ func RestoreJob( PriorityClassName: storage.PriorityClassName, RuntimeClassName: storage.RuntimeClassName, DNSPolicy: corev1.DNSClusterFirst, - SecurityContext: storage.PodSecurityContext, + SecurityContext: k8s.EffectiveBackupStoragePodSecurityContext(cluster, storage), Volumes: []corev1.Volume{ { Name: apiv1.BinVolumeName, @@ -534,9 +534,9 @@ func GetDeleteJob(cluster *apiv1.PerconaServerMySQL, cr *apiv1.PerconaServerMySQ ShareProcessNamespace: &t, SetHostnameAsFQDN: &t, Containers: []corev1.Container{ - deleteContainer(cr.Status.Image, conf, cr, storage), + deleteContainer(cr.Status.Image, conf, cluster, cr, storage), }, - SecurityContext: storage.PodSecurityContext, + SecurityContext: k8s.EffectiveBackupStoragePodSecurityContext(cluster, storage), Affinity: storage.Affinity, TopologySpreadConstraints: storage.TopologySpreadConstraints, Tolerations: storage.Tolerations, @@ -635,7 +635,7 @@ func restoreContainer( Command: []string{"/opt/percona/run-restore.sh"}, TerminationMessagePath: "/dev/termination-log", TerminationMessagePolicy: corev1.TerminationMessageReadFile, - SecurityContext: storage.ContainerSecurityContext, + SecurityContext: k8s.EffectiveBackupStorageContainerSecurityContext(cluster, storage), Resources: storage.Resources, } } diff --git a/pkg/xtrabackup/xtrabackup_test.go b/pkg/xtrabackup/xtrabackup_test.go index 576593080..8eef567bc 100644 --- a/pkg/xtrabackup/xtrabackup_test.go +++ b/pkg/xtrabackup/xtrabackup_test.go @@ -581,6 +581,25 @@ func TestRestoreJob(t *testing.T) { }, getEnv()) }) + t.Run("container security context from mysql with storage override", func(t *testing.T) { + cluster := cr.DeepCopy() + r := backup.DeepCopy() + storage := cluster.Spec.Backup.Storages[storageName] + + mysqlSec := &corev1.SecurityContext{RunAsUser: ptr.To(int64(1001))} + cluster.Spec.MySQL.ContainerSecurityContext = mysqlSec + + j := RestoreJob(cluster, destination, r, storage, initImage, "pvc-name") + assert.Equal(t, mysqlSec, j.Spec.Template.Spec.InitContainers[0].SecurityContext) + assert.Equal(t, mysqlSec, j.Spec.Template.Spec.Containers[0].SecurityContext) + + storageSec := &corev1.SecurityContext{RunAsUser: ptr.To(int64(2002))} + storage.ContainerSecurityContext = storageSec + j = RestoreJob(cluster, destination, r, storage, initImage, "pvc-name") + assert.Equal(t, storageSec, j.Spec.Template.Spec.InitContainers[0].SecurityContext) + assert.Equal(t, storageSec, j.Spec.Template.Spec.Containers[0].SecurityContext) + }) + } func TestGetDestination(t *testing.T) {