diff --git a/instance-applications/120-ibm-dbs-rds-database/templates/00-configmap.yaml b/instance-applications/120-ibm-dbs-rds-database/templates/00-configmap.yaml index 2d34b01f5..26b4684ba 100644 --- a/instance-applications/120-ibm-dbs-rds-database/templates/00-configmap.yaml +++ b/instance-applications/120-ibm-dbs-rds-database/templates/00-configmap.yaml @@ -3,7 +3,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: {{ .Values.scriptConfigMapName | quote }} + name: {{ .Values.db2_rds_instance_name }}-init-script annotations: argocd.argoproj.io/sync-wave: "135" {{- if .Values.custom_labels }} @@ -60,7 +60,7 @@ data: # Creating database connection string for db2rds user conn_rds_db2_str = ( - f"DATABASE={os.environ['DB_NAME']};" + f"DATABASE={database_name};" f"HOSTNAME={os.environ['DB_HOST']};" f"PORT={os.environ['DB_PORT']};" f"PROTOCOL=TCPIP;" @@ -70,13 +70,21 @@ data: f"SSLVersion={ssl_version};" f"SSLServerCertificate={ssl_cert_path};" ) + + # Debug: Print connection strings with masked passwords + masked_admin_str = conn_rds_admin_str.replace(f"PWD={password}", "PWD=***MASKED***") + masked_db2_str = conn_rds_db2_str.replace(f"PWD={password}", "PWD=***MASKED***") + print(f"🔍 Admin connection string: {masked_admin_str}") + print(f"🔍 DB2 connection string: {masked_db2_str}") + # Connect to both databases + # Note: Database availability is checked by bash script before calling this Python script try: admin_conn = db2.connect(conn_rds_admin_str, "", "") - print(f"✅ Connected to Db2 as {username} user") + print(f"✅ Connected to RDS admin database as {username}") - db2_conn = db2.connect(conn_rds_db2_str, "", "") - print("✅ Connected to Db2 as db2rds user") + db2_conn = db2.connect(conn_rds_db2_str, "", "") + print(f"✅ Connected to database '{database_name}'") except Exception as e: print(f"❌ Connection failed: {e}") sys.exit(1) diff --git a/instance-applications/120-ibm-dbs-rds-database/templates/00-db-name-generator-configmap.yaml b/instance-applications/120-ibm-dbs-rds-database/templates/00-db-name-generator-configmap.yaml new file mode 100644 index 000000000..a95692a5b --- /dev/null +++ b/instance-applications/120-ibm-dbs-rds-database/templates/00-db-name-generator-configmap.yaml @@ -0,0 +1,150 @@ +{{- if .Values.application_admin_role }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.db2_rds_instance_name }}-db-common-functions + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +{{- if .Values.custom_labels }} + labels: +{{ .Values.custom_labels | toYaml | indent 4 }} +{{- end }} +data: + db_common_functions.sh: | + #!/bin/bash + # Common functions for DB2 RDS database operations + # Used by both pre-sync (create database) and post-sync (setup) jobs + + # Generate deterministic DB2 database name + generate_db_name() { + local instance_id="$1" + local mas_app_id="$2" + + # Normalize INSTANCE_ID: + # - uppercase + # - remove non-alphanumeric + local inst_clean=$(echo "${instance_id:-}" \ + | tr '[:lower:]' '[:upper:]' \ + | tr -cd 'A-Z0-9') + + # Normalize MAS_APP_ID: + # - remove vowels + # - uppercase + # - remove non-alphanumeric + local app_clean=$(echo "${mas_app_id:-}" \ + | tr -d 'aeiouAEIOU' \ + | tr '[:lower:]' '[:upper:]' \ + | tr -cd 'A-Z0-9') + + # Safety: if app name becomes empty after vowel removal, use default + if [ -z "${app_clean}" ]; then + app_clean="APP" + fi + + # Build fixed-width parts + local inst_part=$(printf "%-5s" "${inst_clean}" | tr ' ' 'X' | cut -c1-5) + local app_part=$(printf "%-3s" "${app_clean}" | tr ' ' 'X' | cut -c1-3) + + local db_name="${inst_part}${app_part}" + + # Ensure DB name starts with a letter (DB2-safe) + case "${db_name}" in + [A-Z]*) + ;; + *) + db_name="D${db_name:1}" + ;; + esac + + echo "${db_name}" + } + + # Wait for database to be available (with polling) + # Uses the Python script from the pre-sync job + wait_for_database() { + local db_name="$1" + local timeout="${2:-600}" # Default 10 minutes + local interval="${3:-10}" # Default 10 seconds + + echo "âŗ Waiting for database '${db_name}' to be available..." + + # Set environment variables for Python script + export DB_NAME="${db_name}" + export DB_CREATE_TIMEOUT_SECONDS="${timeout}" + export DB_POLL_INTERVAL_SECONDS="${interval}" + + # Run the database existence check using Python + python3 -c " + import ibm_db as db2 + import os, sys, re, time, json + + def log(msg): + print(json.dumps(msg)) + + def database_exists(db_name, db_host, db_port, username, password, ssl_cert_path, ssl_version): + try: + conn_str = ( + f'DATABASE={db_name};' + f'HOSTNAME={db_host};' + f'PORT={db_port};' + f'PROTOCOL=TCPIP;' + f'UID={username};' + f'PWD={password};' + f'Security=SSL;' + f'SSLServerCertificate={ssl_cert_path};' + f'SSLVersion={ssl_version};' + ) + conn = db2.connect(conn_str, '', '') + db2.close(conn) + return True + except Exception as e: + msg = str(e) + if any(code in msg for code in ['SQL1013N','SQLSTATE=42705','SQL30061N','SQLSTATE=08004','CLI0199E']): + return False + raise RuntimeError(f'Error checking database existence: {msg}') + + # Read credentials + username = open('/etc/mas/dbs-rds-creds/username').read().strip() + password = open('/etc/mas/dbs-rds-creds/password').read().strip() + + # Get environment variables + db_name = os.getenv('DB_NAME') + db_host = os.getenv('DB_HOST') + db_port = os.getenv('DB_PORT', '50000') + jdbc_url = os.getenv('JDBC_CONNECTION_URL', '') + ssl_cert_path = os.getenv('SSL_CERT_PATH', '/etc/mas/rds-ssl/global-bundle.pem') + timeout = int(os.getenv('DB_CREATE_TIMEOUT_SECONDS', '600')) + interval = int(os.getenv('DB_POLL_INTERVAL_SECONDS', '10')) + + # Extract SSL version + ssl_version = 'TLSv1.2' + if jdbc_url: + m = re.search(r'sslVersion=([^;]+)', jdbc_url, re.IGNORECASE) + if m: + ssl_version = m.group(1) + + # Wait for database + retries = max(1, timeout // interval) + for i in range(retries): + time.sleep(interval) + try: + if database_exists(db_name, db_host, db_port, username, password, ssl_cert_path, ssl_version): + log({'event':'db_available','db':db_name,'elapsed_seconds':(i+1)*interval}) + sys.exit(0) + except RuntimeError as e: + msg = str(e) + if any(x in msg for x in ['SQLSTATE=57019','SQL1035N','SQL30081N']): + log({'event':'retry','attempt':i+1}) + continue + log({'event':'error','msg':msg}) + sys.exit(1) + + log({'event':'timeout','db':db_name,'timeout_seconds':timeout}) + sys.exit(1) + " + return $? + } +{{- end }} + diff --git a/instance-applications/120-ibm-dbs-rds-database/templates/00-presync-create-database.yaml b/instance-applications/120-ibm-dbs-rds-database/templates/00-presync-create-database.yaml index 90c1bb876..1fed3d3df 100644 --- a/instance-applications/120-ibm-dbs-rds-database/templates/00-presync-create-database.yaml +++ b/instance-applications/120-ibm-dbs-rds-database/templates/00-presync-create-database.yaml @@ -100,41 +100,13 @@ spec: if [ -z "${DB_NAME:-}" ]; then echo "â„šī¸ No DB_NAME provided, generating deterministic DB name" - # Normalize INSTANCE_ID: - # - uppercase - # - remove non-alphanumeric - INST_CLEAN=$(echo "${INSTANCE_ID:-}" \ - | tr '[:lower:]' '[:upper:]' \ - | tr -cd 'A-Z0-9') + # Source common functions + source /etc/mas/db-common-functions/db_common_functions.sh - # Normalize MAS_APP_ID: - # - remove vowels - # - uppercase - # - remove non-alphanumeric - APP_CLEAN=$(echo "${MAS_APP_ID:-}" \ - | tr -d 'aeiouAEIOU' \ - | tr '[:lower:]' '[:upper:]' \ - | tr -cd 'A-Z0-9') + # Generate database name using shared function + DB_NAME=$(generate_db_name "${INSTANCE_ID}" "${MAS_APP_ID}") - # Safety: if app name becomes empty after vowel removal, use default - if [ -z "${APP_CLEAN}" ]; then - APP_CLEAN="APP" - fi - - # Build fixed-width parts - INST_PART=$(printf "%-5s" "${INST_CLEAN}" | tr ' ' 'X' | cut -c1-5) - APP_PART=$(printf "%-3s" "${APP_CLEAN}" | tr ' ' 'X' | cut -c1-3) - - DB_NAME="${INST_PART}${APP_PART}" - - # Ensure DB name starts with a letter (DB2-safe) - case "${DB_NAME}" in - [A-Z]*) - ;; - *) - DB_NAME="D${DB_NAME:1}" - ;; - esac + echo "✅ Generated DB_NAME: ${DB_NAME}" else echo "â„šī¸ Using provided DB_NAME: ${DB_NAME}" fi @@ -255,6 +227,8 @@ spec: mountPath: /etc/mas/creds/aws - name: create-db-script mountPath: /etc/mas/scripts/ + - name: db-common-functions + mountPath: /etc/mas/db-common-functions/ volumes: - name: dbs-rds-create-db-secret secret: @@ -266,5 +240,9 @@ spec: configMap: name: {{ .Values.db2_rds_instance_name }}-create-db-script defaultMode: 0755 + - name: db-common-functions + configMap: + name: {{ .Values.db2_rds_instance_name }}-db-common-functions + defaultMode: 0755 {{- end }} diff --git a/instance-applications/120-ibm-dbs-rds-database/templates/01-dbs-rds-postsync-setup.yaml b/instance-applications/120-ibm-dbs-rds-database/templates/01-dbs-rds-postsync-setup.yaml index a985d65ae..dd8f4095e 100644 --- a/instance-applications/120-ibm-dbs-rds-database/templates/01-dbs-rds-postsync-setup.yaml +++ b/instance-applications/120-ibm-dbs-rds-database/templates/01-dbs-rds-postsync-setup.yaml @@ -25,7 +25,7 @@ E.g. passing in a new environment variable. Included in $_job_hash (see below). */}} -{{- $_job_version := "v1" }} +{{- $_job_version := "v2" }} {{- /* 10 char hash appended to the job name taking into account $_job_config_values, $_job_version and $_cli_image_digest @@ -62,9 +62,13 @@ apiVersion: v1 kind: Secret type: Opaque metadata: - name: dbs-rds-secret-store + name: {{ .Values.db2_rds_instance_name }}-secret-store annotations: argocd.argoproj.io/sync-wave: "136" +{{- if .Values.custom_labels }} + labels: +{{ .Values.custom_labels | toYaml | indent 4 }} +{{- end }} data: username: {{ .Values.user | b64enc }} password: {{ .Values.password | b64enc }} @@ -105,6 +109,7 @@ spec: args: - | set -e + echo "Downloading RDS SSL certificate..." mkdir -p /etc/mas/rds-ssl curl -sSL https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem -o /etc/mas/rds-ssl/global-bundle.pem @@ -114,6 +119,29 @@ spec: echo "Verifying ibm_db package..." python3 -c "import ibm_db; print('✅ ibm_db version:', ibm_db.__version__)" + # Source common functions + source /etc/mas/db-common-functions/db_common_functions.sh + + # If DB_NAME is empty, generate it using shared function + if [ -z "${DB_NAME:-}" ]; then + echo "âš ī¸ DB_NAME is empty, generating deterministic database name..." + + # Generate database name using shared function + DB_NAME=$(generate_db_name "${INSTANCE_ID}" "${MAS_APP_ID}") + + echo "✅ Generated DB_NAME: ${DB_NAME}" + export DB_NAME + else + echo "â„šī¸ Using provided DB_NAME: ${DB_NAME}" + fi + + # Wait for database to be available (created by pre-sync job) + if ! wait_for_database "${DB_NAME}" 600 10; then + echo "❌ Database '${DB_NAME}' is not available after waiting" + echo " The pre-sync job may have failed to create the database." + exit 1 + fi + export SSL_CERT_PATH=/etc/mas/rds-ssl/global-bundle.pem && \ python /etc/mas/dbs-rds-init-script/dbs-rds-init.py env: @@ -128,9 +156,11 @@ spec: - name: DB_PORT value: {{ .Values.port | quote }} - name: DB_NAME - value: {{ .Values.dbname | quote }} + value: {{ .Values.dbname | default "" | quote }} - name: SSL_CERT_PATH value: "/etc/mas/rds-ssl/global-bundle.pem" + - name: INSTANCE_ID + value: {{ .Values.instance_id | quote }} {{- if .Values.db2_database_db_config }} - name: DB2_DATABASE_CONFIG value: {{ .Values.db2_database_db_config | toJson | quote }} @@ -140,14 +170,20 @@ spec: mountPath: /etc/mas/dbs-rds-creds/ - name: dbs-rds-init-script mountPath: /etc/mas/dbs-rds-init-script/ + - name: db-common-functions + mountPath: /etc/mas/db-common-functions/ restartPolicy: Never volumes: - name: dbs-rds-init-script configMap: - name: {{ .Values.scriptConfigMapName | quote }} + name: {{ .Values.db2_rds_instance_name }}-init-script - name: dbs-rds-secret-store secret: - secretName: dbs-rds-secret-store + secretName: {{ .Values.db2_rds_instance_name }}-secret-store defaultMode: 420 optional: false + - name: db-common-functions + configMap: + name: {{ .Values.db2_rds_instance_name }}-db-common-functions + defaultMode: 0755 {{- end }} diff --git a/instance-applications/120-ibm-dbs-rds-database/templates/02-backup-script-configmap.yaml b/instance-applications/120-ibm-dbs-rds-database/templates/02-backup-script-configmap.yaml index 514ba7474..d31248afd 100644 --- a/instance-applications/120-ibm-dbs-rds-database/templates/02-backup-script-configmap.yaml +++ b/instance-applications/120-ibm-dbs-rds-database/templates/02-backup-script-configmap.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: "{{ .Values.scriptConfigMapName }}-backup" + name: "{{ .Values.db2_rds_instance_name }}-init-script-backup" annotations: argocd.argoproj.io/sync-wave: "137" {{- if .Values.custom_labels }} diff --git a/instance-applications/120-ibm-dbs-rds-database/templates/03-backup-cronjobs.yaml b/instance-applications/120-ibm-dbs-rds-database/templates/03-backup-cronjobs.yaml index 8e9d33eac..8c0878f65 100644 --- a/instance-applications/120-ibm-dbs-rds-database/templates/03-backup-cronjobs.yaml +++ b/instance-applications/120-ibm-dbs-rds-database/templates/03-backup-cronjobs.yaml @@ -96,11 +96,11 @@ spec: volumes: - name: backup-script configMap: - name: {{ .Values.scriptConfigMapName }}-backup + name: {{ .Values.db2_rds_instance_name }}-init-script-backup defaultMode: 0755 - name: dbs-rds-secret-store secret: - secretName: dbs-rds-secret-store + secretName: {{ .Values.db2_rds_instance_name }}-secret-store defaultMode: 0420 optional: false {{- end }}