apiVersion: argoproj.io/v1alpha1 kind: WorkflowTemplate metadata: name: environment-provisioner namespace: argo-workflows spec: serviceAccountName: argo-workflow # Clean up pods after workflow completion podGC: strategy: OnWorkflowSuccess # Delete pods when workflow succeeds deleteDelayDuration: 60s # Wait 60s before deleting (allows log collection) # Auto-delete workflow after completion ttlStrategy: secondsAfterCompletion: 3600 # Delete workflow 1 hour after completion secondsAfterSuccess: 1800 # Delete workflow 30 min after success secondsAfterFailure: 86400 # Keep failed workflows for 24 hours for debugging arguments: parameters: - name: namespace value: default - name: services value: "" - name: ttl value: "8" - name: owner value: unknown - name: componentMetadata value: '{}' - name: dependencyPreferences value: '{}' - name: valuesFileOverrides value: '{}' - name: manualDependencyUrls value: '{}' - name: valuesContentEdits value: '{}' - name: helmExternalParams value: '{}' entrypoint: provision-environment templates: - inputs: parameters: - name: namespace - name: services - name: ttl - name: owner - name: componentMetadata - name: dependencyPreferences - name: valuesFileOverrides - name: manualDependencyUrls - name: valuesContentEdits - name: helmExternalParams name: provision-environment steps: - - arguments: parameters: - name: namespace value: '{{inputs.parameters.namespace}}' - name: services value: '{{inputs.parameters.services}}' - name: ttl value: '{{inputs.parameters.ttl}}' - name: owner value: '{{inputs.parameters.owner}}' - name: componentMetadata value: '{{inputs.parameters.componentMetadata}}' - name: dependencyPreferences value: '{{inputs.parameters.dependencyPreferences}}' - name: valuesFileOverrides value: '{{inputs.parameters.valuesFileOverrides}}' - name: manualDependencyUrls value: '{{inputs.parameters.manualDependencyUrls}}' - name: valuesContentEdits value: '{{inputs.parameters.valuesContentEdits}}' - name: helmExternalParams value: '{{inputs.parameters.helmExternalParams}}' name: create-git-repo template: create-repo - - arguments: parameters: - name: namespace value: '{{inputs.parameters.namespace}}' - name: owner value: '{{inputs.parameters.owner}}' name: create-argocd-app template: create-argo-app - - arguments: parameters: - name: namespace value: '{{inputs.parameters.namespace}}' - name: services value: '{{inputs.parameters.services}}' name: register-hades-ips template: register-hades-ips - inputs: parameters: - name: namespace - name: services - name: ttl - name: owner - name: componentMetadata - name: dependencyPreferences - name: valuesFileOverrides - name: manualDependencyUrls - name: valuesContentEdits - name: helmExternalParams name: create-repo script: command: - sh env: # Credentials from External Secrets (GCP Secret Manager) - name: GITEA_URL valueFrom: secretKeyRef: name: gitea-credentials key: url - name: GITEA_USER valueFrom: secretKeyRef: name: gitea-credentials key: username - name: GITEA_PASSWORD valueFrom: secretKeyRef: name: gitea-credentials key: password image: alpine/git:latest source: | #!/bin/sh set -e NAMESPACE="{{inputs.parameters.namespace}}" SERVICES="{{inputs.parameters.services}}" TTL="{{inputs.parameters.ttl}}" OWNER="{{inputs.parameters.owner}}" COMPONENT_METADATA=$(cat <<'EOFPARAM' {{inputs.parameters.componentMetadata}} EOFPARAM ) DEPENDENCY_PREFS=$(cat <<'EOFPARAM' {{inputs.parameters.dependencyPreferences}} EOFPARAM ) VALUES_FILE_OVERRIDES=$(cat <<'EOFPARAM' {{inputs.parameters.valuesFileOverrides}} EOFPARAM ) MANUAL_DEPENDENCY_URLS=$(cat <<'EOFPARAM' {{inputs.parameters.manualDependencyUrls}} EOFPARAM ) VALUES_CONTENT_EDITS=$(cat <<'EOFPARAM' {{inputs.parameters.valuesContentEdits}} EOFPARAM ) HELM_EXTERNAL_PARAMS=$(cat <<'EOFPARAM' {{inputs.parameters.helmExternalParams}} EOFPARAM ) # Sanitize namespace name (replace spaces and special chars with hyphens) NAMESPACE=$(echo "$NAMESPACE" | tr '[:upper:]' '[:lower:]' | tr ' _' '-' | sed 's/[^a-z0-9-]//g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//') # Sanitize owner name for labels (Kubernetes labels have strict requirements) OWNER_LABEL=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]' | tr ' _' '-' | sed 's/[^a-z0-9-]//g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//') echo "Creating Git repository for namespace: $NAMESPACE" # Install curl and jq apk add --no-cache curl jq # Create repository in Gitea REPO_NAME="environment-$NAMESPACE" # Extract hostname from GITEA_URL for git credentials GITEA_HOST=$(echo "$GITEA_URL" | sed 's|https://||' | sed 's|http://||' | cut -d'/' -f1) echo "Creating Gitea repository: $REPO_NAME" curl -X POST "$GITEA_URL/api/v1/user/repos" \ -u "$GITEA_USER:$GITEA_PASSWORD" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"$REPO_NAME\", \"description\": \"Environment for $NAMESPACE (owner: $OWNER, ttl: ${TTL}h)\", \"private\": false, \"auto_init\": true }" || echo "Repository may already exist" # Clone the repository git config --global http.sslVerify true git config --global user.email "backstage@osmos.io" git config --global user.name "Backstage" # URL encode the password for git credentials ENCODED_PASSWORD=$(printf %s "$GITEA_PASSWORD" | jq -sRr @uri) # Configure credentials git config --global credential.helper store printf "https://%s:%s@%s\n" "$GITEA_USER" "$ENCODED_PASSWORD" "$GITEA_HOST" > $HOME/.git-credentials REPO_URL="$GITEA_URL/$GITEA_USER/$REPO_NAME.git" git clone "$REPO_URL" /tmp/repo 2>&1 || (cd /tmp/repo && git pull 2>&1) cd /tmp/repo # Create manifests directory mkdir -p manifests # Create namespace manifest cat > manifests/namespace.yaml < manifests/networkpolicy.yaml < "manifests/$VALUES_OVERRIDE_FILE" echo " Created: manifests/$VALUES_OVERRIDE_FILE" # Set inline values for ArgoCD - indent the content properly INDENTED_CONTENT=$(echo "$EDITED_CONTENT" | sed 's/^/ /') INLINE_VALUES=" values: | $INDENTED_CONTENT" fi # Process external params for disabled Helm dependencies (e.g., external database config) CUSTOM_HELM_PARAMS="" SERVICE_EXTERNAL_PARAMS=$(echo "$HELM_EXTERNAL_PARAMS" | jq -r ".\"$service\" // {}") if [ "$SERVICE_EXTERNAL_PARAMS" != "{}" ] && [ -n "$SERVICE_EXTERNAL_PARAMS" ]; then echo " Processing external params for disabled dependencies..." for param_key in $(echo "$SERVICE_EXTERNAL_PARAMS" | jq -r 'keys[]'); do PARAM_VALUE=$(echo "$SERVICE_EXTERNAL_PARAMS" | jq -r ".\"$param_key\"") if [ -n "$PARAM_VALUE" ]; then echo " Setting $param_key = $PARAM_VALUE" # Use param_key directly - dots are Helm nested path separators, not literal chars CUSTOM_HELM_PARAMS="${CUSTOM_HELM_PARAMS} - name: ${param_key} value: \"${PARAM_VALUE}\"" fi done fi # Extract ingress hostname from GITEA_URL domain INGRESS_DOMAIN=$(echo "$GITEA_URL" | sed 's|https://gitea\.||' | sed 's|http://gitea\.||') cat > "manifests/$APP_NAME-application.yaml" < "manifests/$SANITIZED_SERVICE-deployment.yaml" < README.md </dev/null || echo "") if [ -z "$MYSQL_POD" ]; then # Try Bitnami MySQL chart label MYSQL_POD=$(kubectl get pods -n "$NAMESPACE" -l "app.kubernetes.io/name=mysql" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "") fi if [ -z "$MYSQL_POD" ]; then # Fallback: grep for hades mysql in pod names MYSQL_POD=$(kubectl get pods -n "$NAMESPACE" -o name 2>/dev/null | grep -i "hades.*mysql" | head -1 | sed 's/pod\///') fi if [ -z "$MYSQL_POD" ]; then echo " Waiting for MySQL pod... (${POD_WAIT}s)" sleep 10 POD_WAIT=$((POD_WAIT + 10)) fi done if [ -z "$MYSQL_POD" ]; then echo "Warning: Could not find Hades MySQL pod after ${POD_MAX_WAIT}s" echo "Skipping IP registration - Hades may not be deployed" exit 0 fi echo "Found MySQL pod: $MYSQL_POD" # Wait for Hades MySQL to be ready (should already be running from previous steps) echo "Waiting for Hades MySQL to be ready..." MAX_WAIT=120 WAIT_TIME=0 while [ $WAIT_TIME -lt $MAX_WAIT ]; do if kubectl exec -n "$NAMESPACE" "$MYSQL_POD" -- mysql -u "$MYSQL_USER" -p"$MYSQL_PWD" -e "SELECT 1" "$MYSQL_DB" > /dev/null 2>&1; then echo "Hades MySQL is ready" break fi echo " Waiting for MySQL... (${WAIT_TIME}s)" sleep 5 WAIT_TIME=$((WAIT_TIME + 5)) done if [ $WAIT_TIME -ge $MAX_WAIT ]; then echo "Warning: Could not connect to Hades MySQL after ${MAX_WAIT}s" echo "Skipping IP registration - Hades may not be deployed" exit 0 fi # Wait for Hades dbinit job to complete (creates the database schema) echo "Waiting for Hades dbinit job to complete..." DBINIT_MAX_WAIT=120 DBINIT_WAIT_TIME=0 while [ $DBINIT_WAIT_TIME -lt $DBINIT_MAX_WAIT ]; do # Check if dbinit job exists and is complete DBINIT_JOB=$(kubectl get jobs -n "$NAMESPACE" -o name 2>/dev/null | grep -i "hades.*dbinit" | head -1) || DBINIT_JOB="" if [ -n "$DBINIT_JOB" ]; then DBINIT_COMPLETE=$(kubectl get "$DBINIT_JOB" -n "$NAMESPACE" -o jsonpath='{.status.succeeded}' 2>/dev/null) || DBINIT_COMPLETE="0" if [ "$DBINIT_COMPLETE" = "1" ]; then echo "Hades dbinit job completed" break fi fi echo " Waiting for dbinit job... (${DBINIT_WAIT_TIME}s)" sleep 5 DBINIT_WAIT_TIME=$((DBINIT_WAIT_TIME + 5)) done if [ $DBINIT_WAIT_TIME -ge $DBINIT_MAX_WAIT ]; then echo "Warning: Hades dbinit job not completed after ${DBINIT_MAX_WAIT}s" echo "Tables may not exist - attempting registration anyway" fi # Helper function to run MySQL commands via kubectl exec # Using -c to specify container explicitly to avoid kubectl container selection messages MYSQL_CONTAINER=$(kubectl get pod -n "$NAMESPACE" "$MYSQL_POD" -o jsonpath='{.spec.containers[0].name}' 2>/dev/null) echo "MySQL container name: $MYSQL_CONTAINER" run_mysql() { local result local exit_code # Run kubectl exec with explicit container, redirect kubectl stderr to /dev/null # but capture MySQL output (including MySQL warnings on stderr) result=$(kubectl exec -n "$NAMESPACE" "$MYSQL_POD" -c "$MYSQL_CONTAINER" -- \ mysql -u "$MYSQL_USER" -p"$MYSQL_PWD" "$MYSQL_DB" -sN -e "$1" 2>&1) exit_code=$? # Filter out MySQL password warning result=$(echo "$result" | grep -v "Using a password on the command line interface can be insecure") if [ $exit_code -ne 0 ]; then echo " MySQL error: $result" >&2 return 1 fi echo "$result" } # Function to register IP for an app register_ip() { local app_name="$1" local ip="$2" if [ -z "$app_name" ] || [ -z "$ip" ]; then return 0 fi # Get or create app_id (using INSERT ... ON DUPLICATE KEY UPDATE for idempotency) echo " Ensuring app entry exists for: $app_name" if ! run_mysql "INSERT INTO apps (name, token, creation_date, last_update, is_deleted) VALUES ('$app_name', UUID(), NOW(), NOW(), 0) ON DUPLICATE KEY UPDATE last_update=NOW()"; then echo " Warning: INSERT command returned non-zero, but continuing..." fi APP_ID=$(run_mysql "SELECT id FROM apps WHERE name='$app_name'" || echo "") if [ -z "$APP_ID" ]; then echo " Failed to get app_id for: $app_name" return 1 fi echo " Using app_id: $APP_ID" # Register IP (using INSERT IGNORE to handle duplicates gracefully) echo " Registering IP $ip for app $app_name (app_id: $APP_ID)" if run_mysql "INSERT IGNORE INTO app_ips_accesses (app_id, ipaddress, creation_date, last_update, is_deleted) VALUES ($APP_ID, '$ip', NOW(), NOW(), 0)"; then echo " Successfully registered IP $ip" else echo " Failed to register IP $ip" fi return 0 } # Verify required tables exist echo "Checking database tables..." TABLES=$(run_mysql "SHOW TABLES") echo "Available tables: $TABLES" if ! echo "$TABLES" | grep -q "apps"; then echo "ERROR: 'apps' table not found in database" echo "Database may not be initialized. Exiting." exit 1 fi if ! echo "$TABLES" | grep -q "app_ips_accesses"; then echo "ERROR: 'app_ips_accesses' table not found in database" echo "Database may not be initialized. Exiting." exit 1 fi echo "Required tables found. Proceeding with IP registration..." echo "" # 1. Collect common bridge/gateway IPs (handles SNAT across different clusters) COMMON_BRIDGE_IPS="10.244.0.1 10.96.0.1 172.17.0.1" echo "Common bridge IPs: $COMMON_BRIDGE_IPS" # 2. Get node internal IPs NODE_IPS=$(kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null || echo "") echo "Node IPs: $NODE_IPS" # 3. Get pod network CIDR gateways from ALL nodes echo "Collecting pod CIDR gateways from all nodes..." POD_CIDRS=$(kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' 2>/dev/null || echo "") for cidr in $POD_CIDRS; do if [ -n "$cidr" ]; then GATEWAY_IP=$(echo "$cidr" | sed 's/\.[0-9]*\/.*/.1/') COMMON_BRIDGE_IPS="$COMMON_BRIDGE_IPS $GATEWAY_IP" echo " Pod CIDR gateway: $GATEWAY_IP (from $cidr)" fi done # Combine all IPs to register ALL_IPS="$COMMON_BRIDGE_IPS $NODE_IPS" echo "" # Process each service SERVICES_LIST=$(echo "$SERVICES" | tr ',' ' ') for service in $SERVICES_LIST; do service=$(echo "$service" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') [ -z "$service" ] && continue # Skip resource references if echo "$service" | grep -q "^resource:"; then continue fi echo "Processing service: $service" # Get actual pod IPs for this service # Try multiple label selectors as different charts use different labels POD_IPS=$(kubectl get pods -n "$NAMESPACE" \ -l "app.kubernetes.io/instance=${NAMESPACE}-${service}" \ -o jsonpath='{.items[*].status.podIP}' 2>/dev/null) || POD_IPS="" if [ -z "$POD_IPS" ]; then # Try backstage.io/kubernetes-id label POD_IPS=$(kubectl get pods -n "$NAMESPACE" \ -l "backstage.io/kubernetes-id=${service}" \ -o jsonpath='{.items[*].status.podIP}' 2>/dev/null) || POD_IPS="" fi echo " Found pod IPs: ${POD_IPS:-none}" # Register pod IPs for ip in $POD_IPS; do [ -n "$ip" ] && register_ip "$service" "$ip" done # Register bridge/gateway/node IPs for ip in $ALL_IPS; do [ -n "$ip" ] && register_ip "$service" "$ip" done echo "" done # Register IPs for 'nagios' app (used by health probes) echo "Registering IPs for nagios (health probes)..." for ip in $ALL_IPS; do [ -n "$ip" ] && register_ip "nagios" "$ip" done echo "" echo "=== IP Registration Summary ===" kubectl exec -n "$NAMESPACE" "$MYSQL_POD" -- mysql -u "$MYSQL_USER" -p"$MYSQL_PWD" "$MYSQL_DB" -e \ "SELECT a.name as app_name, i.ipaddress FROM apps a JOIN app_ips_accesses i ON a.id = i.app_id WHERE i.is_deleted = 0 ORDER BY a.name, i.ipaddress" 2>/dev/null || echo "Could not retrieve summary" # Update allhosts ConfigMap with registered services echo "" echo "=== Updating allhosts.tsv ConfigMap ===" # Find the allhosts ConfigMap (name depends on Helm release/fullnameOverride) CONFIGMAP_NAME=$(kubectl get configmap -n "$NAMESPACE" -o name 2>/dev/null | grep -E "allhosts$" | head -1 | sed 's|configmap/||') if [ -n "$CONFIGMAP_NAME" ]; then echo "Found allhosts ConfigMap: $CONFIGMAP_NAME" # Get current content CURRENT_CONTENT=$(kubectl get configmap "$CONFIGMAP_NAME" -n "$NAMESPACE" -o jsonpath='{.data.allhosts\.tsv}') # Build new entries for each service NEW_ENTRIES="" for service in $SERVICES_LIST; do service=$(echo "$service" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') [ -z "$service" ] && continue # Skip resource references if echo "$service" | grep -q "^resource:"; then continue fi # Get pod IP for this service POD_IP=$(kubectl get pods -n "$NAMESPACE" \ -l "app.kubernetes.io/instance=${NAMESPACE}-${service}" \ -o jsonpath='{.items[0].status.podIP}' 2>/dev/null) || POD_IP="" if [ -z "$POD_IP" ]; then POD_IP=$(kubectl get pods -n "$NAMESPACE" \ -l "backstage.io/kubernetes-id=${service}" \ -o jsonpath='{.items[0].status.podIP}' 2>/dev/null) || POD_IP="" fi if [ -n "$POD_IP" ]; then HOSTNAME="${service}.${NAMESPACE}.onlinesales.ai" # Check if entry already exists if ! echo "$CURRENT_CONTENT" | grep -q "^${HOSTNAME}[[:space:]]"; then NEW_ENTRY="${HOSTNAME}\t${POD_IP}\t${service}\tplatform\tk8s.pod" NEW_ENTRIES="${NEW_ENTRIES}${NEW_ENTRY}\n" echo " Adding: $HOSTNAME -> $POD_IP" else echo " Skipping (exists): $HOSTNAME" fi fi done if [ -n "$NEW_ENTRIES" ]; then # Append new entries to content UPDATED_CONTENT="${CURRENT_CONTENT} $(echo -e "$NEW_ENTRIES")" # Patch the ConfigMap using kubectl patch (avoids last-applied-configuration warning) kubectl patch configmap "$CONFIGMAP_NAME" -n "$NAMESPACE" \ --type merge -p "{\"data\":{\"allhosts.tsv\":$(echo "$UPDATED_CONTENT" | jq -Rs .)}}" echo "ConfigMap updated successfully" else echo "No new entries to add" fi else echo "Warning: No allhosts ConfigMap found in namespace $NAMESPACE" echo "Looking for ConfigMaps ending with '-allhosts'..." kubectl get configmap -n "$NAMESPACE" -o name 2>/dev/null || echo " No ConfigMaps found" echo "Skipping ConfigMap update" fi echo "" echo "Hades IP registration completed"