mysqloperator/init_main.py (130 lines of code) (raw):

# Copyright (c) 2020, 2025, Oracle and/or its affiliates. # # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ # import subprocess import sys import os import json import logging import shutil import argparse from typing import cast import mysqlsh from .controller import fqdn, utils, k8sobject, config from .controller.api_utils import Edition from .controller.innodbcluster.cluster_api import MySQLPod, InnoDBCluster from .controller.kubeutils import k8s_cluster_domain from .controller.kubeutils import client as api_client, api_core, ApiException k8sobject.g_component = "initconf" k8sobject.g_host = os.getenv("HOSTNAME") mysql = mysqlsh.mysql def get_secret(secret_name: str, namespace: str, logger: logging.Logger) -> dict: if not secret_name: raise Exception(f"No secret provided") ret = {} try: secret = cast(api_client.V1Secret, api_core.read_namespaced_secret(secret_name, namespace)) for k, v in secret.data.items(): ret[k] = utils.b64decode(v) except Exception: raise Exception(f"Secret {secret_name} in namespace {namespace} cannot be found") return ret def init_conf(datadir: str, pod: MySQLPod, cluster, logger: logging.Logger): """ Initialize MySQL configuration files and init scripts, which must be mounted in /mnt/mycnfdata. The source config files must be mounted in /mnt/initconf. Init scripts are executed by the mysql container entrypoint when it's initializing for the 1st time. """ if pod.instance_type == "read-replica": read_replica_name = pod.read_replica_name [rr_spec] = filter(lambda rr: rr.name == read_replica_name, cluster.parsed_spec.readReplicas) server_id = pod.index + rr_spec.baseServerId elif pod.instance_type == "group-member": server_id = pod.index + cluster.parsed_spec.baseServerId else: raise RuntimeError(f"Invalid instance type: {pod.instance_type}") report_host = fqdn.pod_fqdn(pod, logger) logger.info( f"Setting up configurations for {pod.name} server_id={server_id} report_host={report_host}") srcdir = "/mnt/initconf/" destdir = "/mnt/mycnfdata/" mycnf_dir = destdir + "my.cnf.d" initdb_dir = destdir + "docker-entrypoint-initdb.d" os.makedirs(mycnf_dir, exist_ok=True) os.makedirs(initdb_dir, exist_ok=True) with open(srcdir + "my.cnf.in") as f: data = f.read() data = data.replace("@@SERVER_ID@@", str(server_id)) data = data.replace("@@HOSTNAME@@", str(report_host)) data = data.replace("@@DATADIR@@", datadir) with open(destdir + "my.cnf", "w+") as mycnf: mycnf.write(data) for f in os.listdir(srcdir): file = os.path.join(srcdir, f) if f.startswith("initdb-"): print(f"Copying {file} to {initdb_dir}") shutil.copy(file, initdb_dir) if f.endswith(".sh"): os.chmod(os.path.join(initdb_dir, f), 0o555) elif f.endswith(".cnf"): print(f"Copying {file} to {mycnf_dir}") shutil.copy(file, mycnf_dir) logger.info("Configuration done") def init_meb_restore(pod: MySQLPod, cluster: InnoDBCluster, logger: logging.Logger): """Check whether the restore container should restore or not The restore container is based on MySQL Server, not Operator, thus has no access to Kubernetes API etc. in here we make the decision whether this is the first pod of a fresh InnoDB Cluster (i.e. not a recreated -0 for an otherwise running cluster) and leave a marker. We also use tha API to fetch Secrets with credentials for restoring the backup, so that Secret may be deleted afterward""" # Remove Marker as safe fallback try: os.remove('/tmp/meb_restore.json') except FileNotFoundError: # no problem if the file doesn't exist, this would still raise when # failing to remove for permission issues etc which shouldn't happen pass # Check precoditions if not cluster.parsed_spec.initDB or not cluster.parsed_spec.initDB.meb: logger.error("No MySQL Enterprise Restore configured.") return if cluster.parsed_spec.edition != config.Edition.enterprise: print("MySQL Enterprise Restore request, but this is not Enterprise Edition") sys.exit(1) if pod.index: logger.info(f"Nothing to do for restore - restore happens only on Pod 0. this is {pod.name}") return if cluster.get_create_time() is not None: logger.info("Nothing to do for restore - this is a restart") return if pod.instance_type != "group-member": logger.info(f"Nothing to do for restore - this is not a group member but {pod.instance_type}") return # All preconditions are met. We should request a restore. logger.info("We got to request a MEB restore") mebspec = cluster.parsed_spec.initDB.meb if mebspec.oci_credentials: credentials = get_secret(mebspec.oci_credentials, cluster.namespace, logger) elif mebspec.s3_credentials: credentials = get_secret(mebspec.s3_credentials, cluster.namespace, logger) meb_init_spec = { "spec": cluster.spec["initDB"]["meb"], "credentials": credentials } with open('/tmp/meb_restore.json', 'w') as f: json.dump(meb_init_spec, f) def main(argv): # const - when there is an argument without value # default - when there is no argument at all # nargs = "?" - zero or one arguments # nargs = "+" - one or more arguments, returns a list() # nargs = 8 - 8 arguments will be consumed # nargs = 1 - 1 argument will be consumed, returns a list with one element parser = argparse.ArgumentParser(description = "MySQL InnoDB Cluster Instance Sidecar Container") parser.add_argument('--logging-level', type = int, nargs="?", default = logging.INFO, help = "Logging Level") parser.add_argument('--pod-name', type = str, nargs=1, default=None, help = "Pod Name") parser.add_argument('--pod-namespace', type = str, nargs=1, default=None, help = "Pod Namespace") parser.add_argument('--datadir', type = str, default = "/var/lib/mysql", help = "Path do data directory") args = parser.parse_args(argv) datadir = args.datadir mysqlsh.globals.shell.options.useWizards = False logging.basicConfig(level=args.logging_level, format='%(asctime)s - [%(levelname)s] [%(name)s] %(message)s', datefmt="%Y-%m-%dT%H:%M:%S") logger = logging.getLogger("initmysql") name = args.pod_name[0] # nargs returns a list namespace = args.pod_namespace[0] # nargs returns a list logger.info(f"Configuring mysql pod {namespace}/{name}, datadir={datadir}") utils.log_banner(__file__, logger) if logger.level == logging.DEBUG: logger.debug(f"Initial contents of {datadir}:") subprocess.run(["ls", "-l", datadir]) logger.debug("Initial contents of /mnt:") subprocess.run(["ls", "-lR", "/mnt"]) try: pod = MySQLPod.read(name, namespace) cluster = pod.get_cluster() init_conf(datadir, pod, cluster, logger) init_meb_restore(pod, cluster, logger) except Exception as e: import traceback traceback.print_exc() logger.critical(f"Unhandled exception while bootstrapping MySQL: {e}") # TODO post event to the Pod and the Cluster object if this is the seed return 1 # TODO support for restoring from clone snapshot or MEB goes in here return 0