Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions api/v1/perconaservermysql_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
41 changes: 25 additions & 16 deletions build/ps-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/run/secrets/kubernetes.io/serviceaccount/namespace)"
fi

install_keyring_component() {
echo -n '{ "components": "file://component_keyring_vault" }' >/var/lib/mysql/mysqld.my
cp "${KEYRING_VAULT_PATH}" /var/lib/mysql/component_keyring_vault.cnf
Expand Down Expand Up @@ -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}.$(</var/run/secrets/kubernetes.io/serviceaccount/namespace)"
fi

echo '[mysqld]' >$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.
Expand Down Expand Up @@ -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
Expand All @@ -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" \
Expand Down Expand Up @@ -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="$!"

Expand Down Expand Up @@ -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
Expand All @@ -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=$(</var/lib/mysql/full-cluster-crash)
namespace=$(</var/run/secrets/kubernetes.io/serviceaccount/namespace)

Expand All @@ -471,7 +480,7 @@ if [[ -f /var/lib/mysql/full-cluster-crash ]]; then
echo "MySQL pods will be up and running in read only mode."
echo "Latest GTID_EXECUTED on this node is ${gtid_executed}"
echo "If you have spec.mysql.autoRecovery disabled, wait for all pods to be up and running and connect to one of them using mysql-shell:"
echo "kubectl -n ${namespace} exec -it $(hostname) -- mysqlsh root:<password>@localhost"
echo "kubectl -n ${namespace} exec -it $FQDN -- mysqlsh root:<password>@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."
Expand Down
1 change: 1 addition & 0 deletions cmd/example-gen/pkg/defaults/manual.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
3 changes: 2 additions & 1 deletion cmd/example-gen/scripts/lib/ps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"'
Expand Down Expand Up @@ -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)" \
Expand Down
2 changes: 2 additions & 0 deletions config/crd/bases/ps.percona.com_perconaservermysqls.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions deploy/bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions deploy/cr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions deploy/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions deploy/cw-bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 72 additions & 7 deletions e2e-tests/functions
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[shfmt] reported by reviewdog 🐶

Suggested change
if [[ -n "$pod" ]]; then
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() {
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[shfmt] reported by reviewdog 🐶

Suggested change
if [[ "${target_pod}" != "mysql-client" ]]; then
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[shfmt] reported by reviewdog 🐶

Suggested change
if [[ "${target_pod}" != "mysql-client" && "${host}" == *"localhost"* ]]; then
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[shfmt] reported by reviewdog 🐶

Suggested change
if [[ -z "${socket}" ]]; then
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.' || :)
}
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[shfmt] reported by reviewdog 🐶

Suggested change
if [[ "$host" =~ -P[[:space:]]*33060 ]] || [[ "$host" =~ :33060 ]]; then
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
}

Expand Down
8 changes: 8 additions & 0 deletions e2e-tests/tests/auto-config/01-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions e2e-tests/tests/config-router/01-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions e2e-tests/tests/config/01-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/tests/config/09-check-retry-vars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading
Loading