From 20e54c0dd376865df399c2583db0351c8d24a953 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:30:43 +0000 Subject: [PATCH 01/14] Initial plan From 241d7a8a4d11615cb4822c9b994fdff71277c8c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:32:34 +0000 Subject: [PATCH 02/14] fix(security): Remove eval command injection vulnerabilities and unsafe secret handling Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- cli/lib/do_k8s.sh | 23 +++++++++++++++++++---- cli/lib/helm_utils.sh | 23 +++++++++++++---------- cli/lib/stacks.sh | 29 +++++++++++++++++++---------- 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/cli/lib/do_k8s.sh b/cli/lib/do_k8s.sh index df69460..6fbcb36 100644 --- a/cli/lib/do_k8s.sh +++ b/cli/lib/do_k8s.sh @@ -14,7 +14,12 @@ else fi if [ -n "$ENV_FILE" ]; then - export $(grep -v '^#' "$ENV_FILE" | xargs) + # Safely load environment variables from the .env file. + # set -a marks all subsequently defined variables for export. + set -a + # shellcheck source=/dev/null + . "$ENV_FILE" + set +a fi check_doctl() { @@ -89,12 +94,22 @@ create_node_pool() { local count=$4 local tags=$5 # Optional setup as "role=value" + if [ -z "$pool_name" ] || [ -z "$size" ] || [ -z "$count" ]; then + log_error "Usage: create_node_pool [label]" + return 1 + fi + log_info "Creating node pool '$pool_name'..." - local cmd="doctl kubernetes cluster node-pool create $cluster_name --name $pool_name --size $size --count $count" + local args=( + "kubernetes" "cluster" "node-pool" "create" "$cluster_name" + "--name" "$pool_name" + "--size" "$size" + "--count" "$count" + ) if [ -n "$tags" ]; then - cmd="$cmd --label $tags" + args+=("--label" "$tags") fi - eval $cmd + doctl "${args[@]}" } delete_node_pool() { diff --git a/cli/lib/helm_utils.sh b/cli/lib/helm_utils.sh index f920624..0fc9b01 100644 --- a/cli/lib/helm_utils.sh +++ b/cli/lib/helm_utils.sh @@ -24,18 +24,21 @@ deploy_chart() { # Create namespace if needed kubectl create namespace "$namespace" --dry-run=client -o yaml | kubectl apply -f - - local cmd="helm upgrade --install $release_name $chart_path --namespace $namespace" - + # Execute helm upgrade/install with proper argument quoting, avoiding eval if [ -f "$values_file" ]; then - cmd="$cmd -f $values_file" - fi - - # Execute - if eval $cmd; then - log_success "Deployed $release_name successfully." + if helm upgrade --install "$release_name" "$chart_path" --namespace "$namespace" -f "$values_file"; then + log_success "Deployed $release_name successfully." + else + log_error "Failed to deploy $release_name." + return 1 + fi else - log_error "Failed to deploy $release_name." - return 1 + if helm upgrade --install "$release_name" "$chart_path" --namespace "$namespace"; then + log_success "Deployed $release_name successfully." + else + log_error "Failed to deploy $release_name." + return 1 + fi fi } diff --git a/cli/lib/stacks.sh b/cli/lib/stacks.sh index faa30f0..d5463d9 100644 --- a/cli/lib/stacks.sh +++ b/cli/lib/stacks.sh @@ -59,9 +59,13 @@ install_selection() { extra_args="--set installCRDs=true" fi - # External DNS needs DO Token + # External DNS needs DigitalOcean token stored in a Kubernetes Secret if [[ "$rn" == "external-dns" ]]; then - extra_args="--set provider=digitalocean --set digitalocean.apiToken=$DO_TOKEN --set policy=sync --set txtOwnerId=${CLUSTER_NAME:-weown-cluster}" + if [ -z "${DO_TOKEN_SECRET_NAME:-}" ]; then + log_error "ExternalDNS deployment requires DO_TOKEN_SECRET_NAME to be set to the name of a Kubernetes Secret containing the DigitalOcean API token (see docs for secret creation)." + return 1 + fi + extra_args="--set provider=digitalocean --set digitalocean.existingSecret=${DO_TOKEN_SECRET_NAME} --set policy=sync --set txtOwnerId=${CLUSTER_NAME:-weown-cluster}" fi # WordPress: derive domain & email from env if not set in values @@ -197,17 +201,22 @@ install_selection() { # Call helm deploy # Note: extra_args handling needs to be passed to deploy_chart or handled here - # We'll modify deploy_chart to accept extra args in a future refactor, - # for now, we just append to the helm command line inside the function via a hack or direct call. - - # Direct call for flexibility - local cmd="helm upgrade --install $release_name $resolved_chart --namespace $ns --create-namespace $extra_args" + # Build Helm command as an array to avoid eval/command injection + local cmd=(helm upgrade --install "$release_name" "$resolved_chart" --namespace "$ns" --create-namespace) if [ -n "$resolved_values" ]; then - cmd="$cmd -f $resolved_values" + cmd+=(-f "$resolved_values") + fi + if [ -n "${extra_args:-}" ]; then + # Safely split extra_args into an array of arguments (no eval, no command execution) + # shellcheck disable=SC2206 # word splitting here is intentional for Helm flags + local extra_args_array + read -r -a extra_args_array <<< "$extra_args" + cmd+=("${extra_args_array[@]}") fi - log_info "Executing: $cmd" - if eval $cmd; then + # Avoid logging the full command to prevent leaking secrets in extra_args + log_info "Executing Helm upgrade for release '$release_name' in namespace '$ns'." + if "${cmd[@]}"; then log_success "$display_name Installed." # Post-deploy status logs From 047742b3d86abae5e4ee28f49cada1ae452e657c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:33:47 +0000 Subject: [PATCH 03/14] docs: Fix incorrect comments and error message paths Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- n8n/deploy.sh | 8 ++++---- wordpress/helm/templates/secrets.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/n8n/deploy.sh b/n8n/deploy.sh index 38a25ba..a3682a2 100755 --- a/n8n/deploy.sh +++ b/n8n/deploy.sh @@ -246,7 +246,7 @@ install_ingress_nginx() { return 0 fi - log_info "NGINX Ingress Controller not found. Please run ./scripts/03_infra_addons.sh to install shared ingress-nginx before deploying n8n." + log_info "NGINX Ingress Controller not found. Please run './cli/weown' and select the infra stack to install shared ingress-nginx before deploying n8n." exit 1 } @@ -260,7 +260,7 @@ install_cert_manager() { return 0 fi - log_info "cert-manager / ClusterIssuer not detected. Please run ./scripts/03_infra_addons.sh before deploying n8n." + log_info "cert-manager / ClusterIssuer not detected. Please run './cli/weown' and select the infra stack to install shared cert-manager before deploying n8n." exit 1 } @@ -287,7 +287,7 @@ get_external_ip() { log_error "Failed to detect external IP after $max_attempts attempts" echo -e "${YELLOW}Manual steps:${NC}" - echo "1. Check LoadBalancer service: kubectl get svc -n ingress-nginx" + echo "1. Check LoadBalancer service: kubectl get svc -n infra" echo "2. Configure DNS manually once IP is available" exit 1 } @@ -559,7 +559,7 @@ detect_external_ip() { log_error "Failed to detect external IP after $max_attempts attempts" echo -e "${YELLOW}Manual steps:${NC}" - echo "1. Check LoadBalancer service: kubectl get svc -n ingress-nginx" + echo "1. Check LoadBalancer service: kubectl get svc -n infra" echo "2. Configure DNS manually once IP is available" exit 1 } diff --git a/wordpress/helm/templates/secrets.yaml b/wordpress/helm/templates/secrets.yaml index d0dc485..27f9416 100644 --- a/wordpress/helm/templates/secrets.yaml +++ b/wordpress/helm/templates/secrets.yaml @@ -7,7 +7,7 @@ metadata: {{- include "wordpress.labels" . | nindent 4 }} type: Opaque data: - # WordPress admin account created via installation wizard ( to extensively define the visa) + # Password for the initial WordPress administrator account (set via .Values.wordpress.wordpressPassword; raw value is base64-encoded by this template) wordpress-password: {{ .Values.wordpress.wordpressPassword | default "" | b64enc | quote }} # MariaDB passwords (generated by deployment script) From 97e50032b5afe7df55450bdbc8a7e8af9ae2e0c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:34:57 +0000 Subject: [PATCH 04/14] fix(config): Remove unused function, pin K8s version, fix NetworkPolicy namespace mismatches Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- cli/lib/do_k8s.sh | 2 +- cli/lib/stacks.sh | 5 +++++ n8n/deploy.sh | 28 ---------------------------- wordpress/deploy.sh | 18 ++++++++++++++---- 4 files changed, 20 insertions(+), 33 deletions(-) diff --git a/cli/lib/do_k8s.sh b/cli/lib/do_k8s.sh index 6fbcb36..b8d7d5f 100644 --- a/cli/lib/do_k8s.sh +++ b/cli/lib/do_k8s.sh @@ -46,7 +46,7 @@ check_doctl() { ensure_cluster() { local cluster_name=${1:-${CLUSTER_NAME:-weown-cluster}} local region=${2:-${DO_REGION:-nyc3}} - local version="latest" + local version=${K8S_VERSION:-"1.33.1-do.0"} log_info "Checking for cluster: $cluster_name..." diff --git a/cli/lib/stacks.sh b/cli/lib/stacks.sh index d5463d9..b8fa80a 100644 --- a/cli/lib/stacks.sh +++ b/cli/lib/stacks.sh @@ -90,6 +90,11 @@ install_selection() { extra_args+=" --set wordpress.wordpressPassword=${wp_admin_password}" extra_args+=" --set ingress.hosts[0].host=${wp_domain}" extra_args+=" --set ingress.tls[0].hosts[0]=${wp_domain}" + + # Override NetworkPolicy to allow ingress from 'infra' namespace (where ingress-nginx is installed) + # The default NetworkPolicy expects namespace labeled 'name=ingress-nginx', but CLI installs to 'infra' + extra_args+=" --set networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.kubernetes\\.io/metadata\\.name=infra" + # Optionally wire email into WordPress config if [ -n "${LETSENCRYPT_EMAIL:-}" ]; then extra_args+=" --set wordpress.wordpressEmail=${LETSENCRYPT_EMAIL}" diff --git a/n8n/deploy.sh b/n8n/deploy.sh index a3682a2..9181492 100755 --- a/n8n/deploy.sh +++ b/n8n/deploy.sh @@ -264,34 +264,6 @@ install_cert_manager() { exit 1 } -# Get external IP -get_external_ip() { - log_step "Detecting external IP address..." - - local max_attempts=60 - local attempt=1 - - while [[ $attempt -le $max_attempts ]]; do - EXTERNAL_IP=$(kubectl get service ingress-nginx-controller -n infra \ - -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "") - - if [[ -n "$EXTERNAL_IP" && "$EXTERNAL_IP" != "null" ]]; then - log_success "External IP detected: $EXTERNAL_IP" - return 0 - fi - - log_info "Waiting for external IP... (attempt $attempt/$max_attempts)" - sleep 5 - ((attempt++)) - done - - log_error "Failed to detect external IP after $max_attempts attempts" - echo -e "${YELLOW}Manual steps:${NC}" - echo "1. Check LoadBalancer service: kubectl get svc -n infra" - echo "2. Configure DNS manually once IP is available" - exit 1 -} - validate_dns() { log_step "Validating DNS configuration for Let's Encrypt..." diff --git a/wordpress/deploy.sh b/wordpress/deploy.sh index 3956294..046a6f3 100755 --- a/wordpress/deploy.sh +++ b/wordpress/deploy.sh @@ -597,10 +597,20 @@ check_prerequisites() { setup_infrastructure() { log_step "Setting Up Infrastructure Prerequisites" - if kubectl get svc ingress-nginx-controller -n infra >/dev/null 2>&1 \ - && kubectl get clusterissuer letsencrypt-prod >/dev/null 2>&1; then - log_substep "Using existing ingress-nginx controller and ClusterIssuer 'letsencrypt-prod' (shared infra); skipping local installation" - return 0 + if kubectl get svc ingress-nginx-controller -n infra >/dev/null 2>&1; then + # Ensure infra namespace has the required label for WordPress NetworkPolicy + if ! kubectl get namespace infra --show-labels | grep -q "name=ingress-nginx"; then + log_substep "Adding required NetworkPolicy label to infra namespace for shared ingress..." + kubectl label namespace infra name=ingress-nginx --overwrite + log_substep "✓ NetworkPolicy label added to infra namespace" + else + log_substep "✓ infra namespace already labeled for NetworkPolicy access" + fi + + if kubectl get clusterissuer letsencrypt-prod >/dev/null 2>&1; then + log_substep "Using existing ingress-nginx controller in 'infra' and ClusterIssuer 'letsencrypt-prod' (shared infra); skipping local installation" + return 0 + fi fi # Get cluster name for LoadBalancer naming (extract from current context) From d3eb3ec3033f87ad1ec142de2e71cfdb7ed7da33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 05:36:23 +0000 Subject: [PATCH 05/14] fix: Remove unnecessary NetworkPolicy override (handled by namespace labeling) Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- cli/lib/stacks.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/lib/stacks.sh b/cli/lib/stacks.sh index b8fa80a..28769a4 100644 --- a/cli/lib/stacks.sh +++ b/cli/lib/stacks.sh @@ -91,10 +91,6 @@ install_selection() { extra_args+=" --set ingress.hosts[0].host=${wp_domain}" extra_args+=" --set ingress.tls[0].hosts[0]=${wp_domain}" - # Override NetworkPolicy to allow ingress from 'infra' namespace (where ingress-nginx is installed) - # The default NetworkPolicy expects namespace labeled 'name=ingress-nginx', but CLI installs to 'infra' - extra_args+=" --set networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.kubernetes\\.io/metadata\\.name=infra" - # Optionally wire email into WordPress config if [ -n "${LETSENCRYPT_EMAIL:-}" ]; then extra_args+=" --set wordpress.wordpressEmail=${LETSENCRYPT_EMAIL}" From 08cbbb22102bea917e0fba779e977a74bbc95dc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 08:32:04 +0000 Subject: [PATCH 06/14] fix: Refactor to use arrays throughout, fix DigitalOcean secret params, improve .env security Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- cli/lib/do_k8s.sh | 40 +++++++++++++++--- cli/lib/helm_utils.sh | 20 ++++----- cli/lib/stacks.sh | 94 ++++++++++++++++++++++--------------------- 3 files changed, 91 insertions(+), 63 deletions(-) diff --git a/cli/lib/do_k8s.sh b/cli/lib/do_k8s.sh index b8d7d5f..5e6e9fc 100644 --- a/cli/lib/do_k8s.sh +++ b/cli/lib/do_k8s.sh @@ -14,12 +14,40 @@ else fi if [ -n "$ENV_FILE" ]; then - # Safely load environment variables from the .env file. - # set -a marks all subsequently defined variables for export. - set -a - # shellcheck source=/dev/null - . "$ENV_FILE" - set +a + # Safely load environment variables from the .env file without executing it as shell. + # Only accept simple KEY=VALUE lines, ignore comments and malformed entries. + while IFS= read -r line || [ -n "$line" ]; do + # Trim leading and trailing whitespace + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + + # Skip empty lines and comments + if [ -z "$line" ] || [ "${line#\#}" != "$line" ]; then + continue + fi + + # Require KEY=VALUE form with a safe variable name + case "$line" in + [A-Za-z_][A-Za-z0-9_]*=*) + key=${line%%=*} + value=${line#*=} + + # Strip optional surrounding single or double quotes from value + if [ "${value#\"}" != "$value" ] && [ "${value%\"}" != "$value" ]; then + value=${value#\"} + value=${value%\"} + elif [ "${value#\'}" != "$value" ] && [ "${value%\'}" != "$value" ]; then + value=${value#\'} + value=${value%\'} + fi + + export "$key=$value" + ;; + *) + # Ignore lines that are not simple KEY=VALUE assignments + ;; + esac + done < "$ENV_FILE" fi check_doctl() { diff --git a/cli/lib/helm_utils.sh b/cli/lib/helm_utils.sh index 0fc9b01..0bfdce7 100644 --- a/cli/lib/helm_utils.sh +++ b/cli/lib/helm_utils.sh @@ -25,20 +25,16 @@ deploy_chart() { kubectl create namespace "$namespace" --dry-run=client -o yaml | kubectl apply -f - # Execute helm upgrade/install with proper argument quoting, avoiding eval + local cmd=(helm upgrade --install "$release_name" "$chart_path" --namespace "$namespace") if [ -f "$values_file" ]; then - if helm upgrade --install "$release_name" "$chart_path" --namespace "$namespace" -f "$values_file"; then - log_success "Deployed $release_name successfully." - else - log_error "Failed to deploy $release_name." - return 1 - fi + cmd+=(-f "$values_file") + fi + + if "${cmd[@]}"; then + log_success "Deployed $release_name successfully." else - if helm upgrade --install "$release_name" "$chart_path" --namespace "$namespace"; then - log_success "Deployed $release_name successfully." - else - log_error "Failed to deploy $release_name." - return 1 - fi + log_error "Failed to deploy $release_name." + return 1 fi } diff --git a/cli/lib/stacks.sh b/cli/lib/stacks.sh index 28769a4..a4ffe9a 100644 --- a/cli/lib/stacks.sh +++ b/cli/lib/stacks.sh @@ -53,19 +53,24 @@ install_selection() { # Special handling for infra apps (flags passed via cli often, not just values) # This is a simplified logic. Real "extensive" logic would have specific functions per app. + # Build extra_args as an array from the start to safely handle values with spaces + local extra_args_array=() + # E.g., Cert-Manager needs --set installCRDs=true - local extra_args="" if [[ "$rn" == "cert-manager" ]]; then - extra_args="--set installCRDs=true" + extra_args_array+=(--set installCRDs=true) fi # External DNS needs DigitalOcean token stored in a Kubernetes Secret if [[ "$rn" == "external-dns" ]]; then if [ -z "${DO_TOKEN_SECRET_NAME:-}" ]; then - log_error "ExternalDNS deployment requires DO_TOKEN_SECRET_NAME to be set to the name of a Kubernetes Secret containing the DigitalOcean API token (see docs for secret creation)." + log_error "ExternalDNS deployment requires DO_TOKEN_SECRET_NAME to be set to the name of a Kubernetes Secret containing the DigitalOcean API token under the 'digitalocean_api_token' key. Create this Secret securely using an env file (no --from-literal); see the docs for the exact kubectl command." return 1 fi - extra_args="--set provider=digitalocean --set digitalocean.existingSecret=${DO_TOKEN_SECRET_NAME} --set policy=sync --set txtOwnerId=${CLUSTER_NAME:-weown-cluster}" + extra_args_array+=(--set provider=digitalocean) + extra_args_array+=(--set digitalocean.secretName="${DO_TOKEN_SECRET_NAME}") + extra_args_array+=(--set policy=sync) + extra_args_array+=(--set txtOwnerId="${CLUSTER_NAME:-weown-cluster}") fi # WordPress: derive domain & email from env if not set in values @@ -86,14 +91,14 @@ install_selection() { return 1 fi # Populate chart values overriding placeholders / empty hosts - extra_args+=" --set wordpress.domain=${wp_domain}" - extra_args+=" --set wordpress.wordpressPassword=${wp_admin_password}" - extra_args+=" --set ingress.hosts[0].host=${wp_domain}" - extra_args+=" --set ingress.tls[0].hosts[0]=${wp_domain}" + extra_args_array+=(--set "wordpress.domain=${wp_domain}") + extra_args_array+=(--set "wordpress.wordpressPassword=${wp_admin_password}") + extra_args_array+=(--set "ingress.hosts[0].host=${wp_domain}") + extra_args_array+=(--set "ingress.tls[0].hosts[0]=${wp_domain}") # Optionally wire email into WordPress config if [ -n "${LETSENCRYPT_EMAIL:-}" ]; then - extra_args+=" --set wordpress.wordpressEmail=${LETSENCRYPT_EMAIL}" + extra_args_array+=(--set "wordpress.wordpressEmail=${LETSENCRYPT_EMAIL}") fi fi @@ -107,15 +112,15 @@ install_selection() { log_error "n8n deployment requires N8N_DOMAIN or BASE_DOMAIN in .env to derive the ingress host." return 1 fi - extra_args+=" --set global.domain=${n8n_domain}" + extra_args_array+=(--set "global.domain=${n8n_domain}") if [ -n "${LETSENCRYPT_EMAIL:-}" ]; then - extra_args+=" --set global.email=${LETSENCRYPT_EMAIL}" + extra_args_array+=(--set "global.email=${LETSENCRYPT_EMAIL}") fi - extra_args+=" --set n8n.config.N8N_HOST=${n8n_domain}" - extra_args+=" --set n8n.config.WEBHOOK_URL=https://${n8n_domain}/" - extra_args+=" --set ingress.hosts[0].host=${n8n_domain}" - extra_args+=" --set ingress.tls[0].hosts[0]=${n8n_domain}" - extra_args+=" --set networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra" + extra_args_array+=(--set "n8n.config.N8N_HOST=${n8n_domain}") + extra_args_array+=(--set "n8n.config.WEBHOOK_URL=https://${n8n_domain}/") + extra_args_array+=(--set "ingress.hosts[0].host=${n8n_domain}") + extra_args_array+=(--set "ingress.tls[0].hosts[0]=${n8n_domain}") + extra_args_array+=(--set "networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra") fi # Nextcloud: derive domain from env to replace DOMAIN_PLACEHOLDER @@ -128,15 +133,15 @@ install_selection() { log_error "Nextcloud deployment requires NEXTCLOUD_DOMAIN or BASE_DOMAIN in .env to derive the ingress host." return 1 fi - extra_args+=" --set global.domain=${nextcloud_domain}" + extra_args_array+=(--set "global.domain=${nextcloud_domain}") if [ -n "${LETSENCRYPT_EMAIL:-}" ]; then - extra_args+=" --set global.email=${LETSENCRYPT_EMAIL}" + extra_args_array+=(--set "global.email=${LETSENCRYPT_EMAIL}") fi - extra_args+=" --set nextcloud.config.NEXTCLOUD_HOST=${nextcloud_domain}" - extra_args+=" --set nextcloud.config.NEXTCLOUD_TRUSTED_DOMAINS=${nextcloud_domain}" - extra_args+=" --set ingress.hosts[0].host=${nextcloud_domain}" - extra_args+=" --set ingress.tls[0].hosts[0]=${nextcloud_domain}" - extra_args+=" --set networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra" + extra_args_array+=(--set "nextcloud.config.NEXTCLOUD_HOST=${nextcloud_domain}") + extra_args_array+=(--set "nextcloud.config.NEXTCLOUD_TRUSTED_DOMAINS=${nextcloud_domain}") + extra_args_array+=(--set "ingress.hosts[0].host=${nextcloud_domain}") + extra_args_array+=(--set "ingress.tls[0].hosts[0]=${nextcloud_domain}") + extra_args_array+=(--set "networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra") fi # Matomo: derive domain & tracking host from env to replace DOMAIN_PLACEHOLDER @@ -162,18 +167,18 @@ install_selection() { return 1 fi # Override ingress + global + website host and DB auth values - extra_args+=" --set global.domain=${matomo_domain}" + extra_args_array+=(--set "global.domain=${matomo_domain}") if [ -n "${LETSENCRYPT_EMAIL:-}" ]; then - extra_args+=" --set global.email=${LETSENCRYPT_EMAIL}" - extra_args+=" --set matomo.admin.email=${LETSENCRYPT_EMAIL}" - fi - extra_args+=" --set mariadb.auth.rootPassword=${matomo_db_root_password}" - extra_args+=" --set mariadb.auth.password=${matomo_db_password}" - extra_args+=" --set ingress.hosts[0].host=${matomo_domain}" - extra_args+=" --set ingress.tls[0].hosts[0]=${matomo_domain}" - extra_args+=" --set networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra" + extra_args_array+=(--set "global.email=${LETSENCRYPT_EMAIL}") + extra_args_array+=(--set "matomo.admin.email=${LETSENCRYPT_EMAIL}") + fi + extra_args_array+=(--set "mariadb.auth.rootPassword=${matomo_db_root_password}") + extra_args_array+=(--set "mariadb.auth.password=${matomo_db_password}") + extra_args_array+=(--set "ingress.hosts[0].host=${matomo_domain}") + extra_args_array+=(--set "ingress.tls[0].hosts[0]=${matomo_domain}") + extra_args_array+=(--set "networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra") if [ -n "$wp_tracking_host" ]; then - extra_args+=" --set matomo.website.host=${wp_tracking_host}" + extra_args_array+=(--set "matomo.website.host=${wp_tracking_host}") fi fi @@ -188,30 +193,29 @@ install_selection() { log_error "Vaultwarden deployment requires VAULTWARDEN_DOMAIN or BASE_DOMAIN in .env to derive the domain." return 1 fi - extra_args+=" --set global.domain=${vaultwarden_domain}" - extra_args+=" --set global.subdomain=${vaultwarden_subdomain}" + extra_args_array+=(--set "global.domain=${vaultwarden_domain}") + extra_args_array+=(--set "global.subdomain=${vaultwarden_subdomain}") if [ -n "${LETSENCRYPT_EMAIL:-}" ]; then - extra_args+=" --set certManager.email=${LETSENCRYPT_EMAIL}" + extra_args_array+=(--set "certManager.email=${LETSENCRYPT_EMAIL}") fi - extra_args+=" --set vaultwarden.domain=https://${vaultwarden_subdomain}.${vaultwarden_domain}" - extra_args+=" --set security.networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra" + extra_args_array+=(--set "vaultwarden.domain=https://${vaultwarden_subdomain}.${vaultwarden_domain}") + extra_args_array+=(--set "security.networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra") fi log_info "Processing $display_name ($release_name)..." - log_info "Helm extra args: ${extra_args:-}" + if [ ${#extra_args_array[@]} -gt 0 ]; then + log_info "Helm extra args count: ${#extra_args_array[@]}" + else + log_info "Helm extra args: " + fi # Call helm deploy - # Note: extra_args handling needs to be passed to deploy_chart or handled here # Build Helm command as an array to avoid eval/command injection local cmd=(helm upgrade --install "$release_name" "$resolved_chart" --namespace "$ns" --create-namespace) if [ -n "$resolved_values" ]; then cmd+=(-f "$resolved_values") fi - if [ -n "${extra_args:-}" ]; then - # Safely split extra_args into an array of arguments (no eval, no command execution) - # shellcheck disable=SC2206 # word splitting here is intentional for Helm flags - local extra_args_array - read -r -a extra_args_array <<< "$extra_args" + if [ ${#extra_args_array[@]} -gt 0 ]; then cmd+=("${extra_args_array[@]}") fi From b71973688610dca7a262760f25ffe38ec5141a1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 08:33:10 +0000 Subject: [PATCH 07/14] refactor: Improve .env parser quote handling and add explanatory comments Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- cli/lib/do_k8s.sh | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/cli/lib/do_k8s.sh b/cli/lib/do_k8s.sh index 5e6e9fc..755d755 100644 --- a/cli/lib/do_k8s.sh +++ b/cli/lib/do_k8s.sh @@ -17,7 +17,9 @@ if [ -n "$ENV_FILE" ]; then # Safely load environment variables from the .env file without executing it as shell. # Only accept simple KEY=VALUE lines, ignore comments and malformed entries. while IFS= read -r line || [ -n "$line" ]; do - # Trim leading and trailing whitespace + # Trim leading and trailing whitespace using parameter expansion + # ${var##*[![:space:]]} finds the last non-whitespace char, then % removes trailing whitespace + # ${var%%[![:space:]]*} finds the first non-whitespace char, then # removes leading whitespace line="${line#"${line%%[![:space:]]*}"}" line="${line%"${line##*[![:space:]]}"}" @@ -32,14 +34,18 @@ if [ -n "$ENV_FILE" ]; then key=${line%%=*} value=${line#*=} - # Strip optional surrounding single or double quotes from value - if [ "${value#\"}" != "$value" ] && [ "${value%\"}" != "$value" ]; then - value=${value#\"} - value=${value%\"} - elif [ "${value#\'}" != "$value" ] && [ "${value%\'}" != "$value" ]; then - value=${value#\'} - value=${value%\'} - fi + # Strip matching surrounding quotes (both double or both single) + # Only strip if quote appears at both start AND end + case "$value" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac export "$key=$value" ;; From 4dd6765c95095ce81c77ca321d5f441235ce3aaa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:23:06 +0000 Subject: [PATCH 08/14] fix(namespace): Standardize on ingress-nginx namespace, remove incorrect infra overrides Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- cli/lib/stacks.sh | 18 ++++++++++-------- n8n/deploy.sh | 16 ++++++++-------- wordpress/deploy.sh | 16 ++++++++-------- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/cli/lib/stacks.sh b/cli/lib/stacks.sh index a4ffe9a..ccc034d 100644 --- a/cli/lib/stacks.sh +++ b/cli/lib/stacks.sh @@ -6,10 +6,10 @@ source "$(dirname "${BASH_SOURCE[0]}")/do_k8s.sh" # Define available stacks/apps # Format: "DisplayName|ReleaseName|Description|ChartPath|Namespace|ValuesFile" APPS=( - "Infra: Nginx Ingress|ingress-nginx|Core ingress controller|ingress-nginx/ingress-nginx|infra|" - "Infra: Cert-Manager|cert-manager|SSL Certificates|jetstack/cert-manager|infra|" - "Infra: ExternalDNS|external-dns|DO DNS Sync|bitnami/external-dns|infra|" - "Infra: Monitoring|kube-prometheus-stack|Prometheus & Grafana|prometheus-community/kube-prometheus-stack|infra|" + "Infra: Nginx Ingress|ingress-nginx|Core ingress controller|ingress-nginx/ingress-nginx|ingress-nginx|" + "Infra: Cert-Manager|cert-manager|SSL Certificates|jetstack/cert-manager|cert-manager|" + "Infra: ExternalDNS|external-dns|DO DNS Sync|bitnami/external-dns|external-dns|" + "Infra: Monitoring|kube-prometheus-stack|Prometheus & Grafana|prometheus-community/kube-prometheus-stack|monitoring|" "App: WordPress|wordpress|CMS Blog|wordpress/helm|wordpress|wordpress/helm/values.yaml" "App: n8n|n8n|Workflow Automation|n8n/helm|n8n|n8n/helm/values.yaml" "App: Matomo|matomo|Web Analytics|matomo/helm|matomo|matomo/helm/values.yaml" @@ -120,7 +120,6 @@ install_selection() { extra_args_array+=(--set "n8n.config.WEBHOOK_URL=https://${n8n_domain}/") extra_args_array+=(--set "ingress.hosts[0].host=${n8n_domain}") extra_args_array+=(--set "ingress.tls[0].hosts[0]=${n8n_domain}") - extra_args_array+=(--set "networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra") fi # Nextcloud: derive domain from env to replace DOMAIN_PLACEHOLDER @@ -141,7 +140,6 @@ install_selection() { extra_args_array+=(--set "nextcloud.config.NEXTCLOUD_TRUSTED_DOMAINS=${nextcloud_domain}") extra_args_array+=(--set "ingress.hosts[0].host=${nextcloud_domain}") extra_args_array+=(--set "ingress.tls[0].hosts[0]=${nextcloud_domain}") - extra_args_array+=(--set "networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra") fi # Matomo: derive domain & tracking host from env to replace DOMAIN_PLACEHOLDER @@ -176,7 +174,6 @@ install_selection() { extra_args_array+=(--set "mariadb.auth.password=${matomo_db_password}") extra_args_array+=(--set "ingress.hosts[0].host=${matomo_domain}") extra_args_array+=(--set "ingress.tls[0].hosts[0]=${matomo_domain}") - extra_args_array+=(--set "networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra") if [ -n "$wp_tracking_host" ]; then extra_args_array+=(--set "matomo.website.host=${wp_tracking_host}") fi @@ -199,7 +196,6 @@ install_selection() { extra_args_array+=(--set "certManager.email=${LETSENCRYPT_EMAIL}") fi extra_args_array+=(--set "vaultwarden.domain=https://${vaultwarden_subdomain}.${vaultwarden_domain}") - extra_args_array+=(--set "security.networkPolicy.ingress[0].from[0].namespaceSelector.matchLabels.name=infra") fi log_info "Processing $display_name ($release_name)..." @@ -224,6 +220,12 @@ install_selection() { if "${cmd[@]}"; then log_success "$display_name Installed." + # Special post-deployment for ingress-nginx: label namespace for NetworkPolicy + if [[ "$rn" == "ingress-nginx" ]]; then + kubectl label namespace "$ns" name=ingress-nginx --overwrite + log_success "Labeled namespace '$ns' with name=ingress-nginx for NetworkPolicy access" + fi + # Post-deploy status logs log_info "Helm status for $release_name (namespace: $ns):" helm status "$release_name" -n "$ns" || log_warn "helm status failed for $release_name in $ns" diff --git a/n8n/deploy.sh b/n8n/deploy.sh index 9181492..1322e65 100755 --- a/n8n/deploy.sh +++ b/n8n/deploy.sh @@ -234,9 +234,9 @@ check_prerequisites() { install_ingress_nginx() { log_step "Checking NGINX Ingress Controller..." - # Prefer shared ingress-nginx installed by infra add-ons in namespace 'infra' - if kubectl get svc ingress-nginx-controller -n infra &> /dev/null; then - log_success "Using shared ingress-nginx controller in namespace 'infra'" + # Prefer shared ingress-nginx installed by infra add-ons in namespace 'ingress-nginx' + if kubectl get svc ingress-nginx-controller -n ingress-nginx &> /dev/null; then + log_success "Using shared ingress-nginx controller in namespace 'ingress-nginx'" return 0 fi @@ -246,7 +246,7 @@ install_ingress_nginx() { return 0 fi - log_info "NGINX Ingress Controller not found. Please run './cli/weown' and select the infra stack to install shared ingress-nginx before deploying n8n." + log_info "NGINX Ingress Controller not found. Please run './cli/weown' and select the ingress-nginx stack to install shared ingress-nginx before deploying n8n." exit 1 } @@ -254,13 +254,13 @@ install_ingress_nginx() { install_cert_manager() { log_step "Checking cert-manager..." - # Prefer shared cert-manager and ClusterIssuer created by infra add-ons + # Prefer shared cert-manager and ClusterIssuer created by infra add-ons (cert-manager stack) if kubectl get clusterissuer letsencrypt-prod &> /dev/null; then log_success "Using shared cert-manager / ClusterIssuer 'letsencrypt-prod'" return 0 fi - log_info "cert-manager / ClusterIssuer not detected. Please run './cli/weown' and select the infra stack to install shared cert-manager before deploying n8n." + log_info "cert-manager / ClusterIssuer not detected. Please run './cli/weown' and select the cert-manager stack to install shared cert-manager before deploying n8n." exit 1 } @@ -517,7 +517,7 @@ detect_external_ip() { local attempt=1 while [[ $attempt -le $max_attempts ]]; do - EXTERNAL_IP=$(kubectl get service ingress-nginx-controller -n infra -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "") + EXTERNAL_IP=$(kubectl get service ingress-nginx-controller -n ingress-nginx -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "") if [[ -n "$EXTERNAL_IP" && "$EXTERNAL_IP" != "null" ]]; then log_success "External IP detected: $EXTERNAL_IP" @@ -531,7 +531,7 @@ detect_external_ip() { log_error "Failed to detect external IP after $max_attempts attempts" echo -e "${YELLOW}Manual steps:${NC}" - echo "1. Check LoadBalancer service: kubectl get svc -n infra" + echo "1. Check LoadBalancer service: kubectl get svc -n ingress-nginx" echo "2. Configure DNS manually once IP is available" exit 1 } diff --git a/wordpress/deploy.sh b/wordpress/deploy.sh index 046a6f3..9c40447 100755 --- a/wordpress/deploy.sh +++ b/wordpress/deploy.sh @@ -597,18 +597,18 @@ check_prerequisites() { setup_infrastructure() { log_step "Setting Up Infrastructure Prerequisites" - if kubectl get svc ingress-nginx-controller -n infra >/dev/null 2>&1; then - # Ensure infra namespace has the required label for WordPress NetworkPolicy - if ! kubectl get namespace infra --show-labels | grep -q "name=ingress-nginx"; then - log_substep "Adding required NetworkPolicy label to infra namespace for shared ingress..." - kubectl label namespace infra name=ingress-nginx --overwrite - log_substep "✓ NetworkPolicy label added to infra namespace" + if kubectl get svc ingress-nginx-controller -n ingress-nginx >/dev/null 2>&1; then + # Ensure ingress-nginx namespace has the required label for WordPress NetworkPolicy + if ! kubectl get namespace ingress-nginx --show-labels | grep -q "name=ingress-nginx"; then + log_substep "Adding required NetworkPolicy label to ingress-nginx namespace for shared ingress..." + kubectl label namespace ingress-nginx name=ingress-nginx --overwrite + log_substep "✓ NetworkPolicy label added to ingress-nginx namespace" else - log_substep "✓ infra namespace already labeled for NetworkPolicy access" + log_substep "✓ ingress-nginx namespace already labeled for NetworkPolicy access" fi if kubectl get clusterissuer letsencrypt-prod >/dev/null 2>&1; then - log_substep "Using existing ingress-nginx controller in 'infra' and ClusterIssuer 'letsencrypt-prod' (shared infra); skipping local installation" + log_substep "Using existing ingress-nginx controller in 'ingress-nginx' and ClusterIssuer 'letsencrypt-prod' (shared infra); skipping local installation" return 0 fi fi From c5b2d3b7bd9c5262b2bdf6edaa5219e2ea8f77ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:24:04 +0000 Subject: [PATCH 09/14] fix: Improve namespace labeling logic and error handling Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- cli/lib/stacks.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cli/lib/stacks.sh b/cli/lib/stacks.sh index ccc034d..83f4d4e 100644 --- a/cli/lib/stacks.sh +++ b/cli/lib/stacks.sh @@ -220,10 +220,13 @@ install_selection() { if "${cmd[@]}"; then log_success "$display_name Installed." - # Special post-deployment for ingress-nginx: label namespace for NetworkPolicy - if [[ "$rn" == "ingress-nginx" ]]; then - kubectl label namespace "$ns" name=ingress-nginx --overwrite - log_success "Labeled namespace '$ns' with name=ingress-nginx for NetworkPolicy access" + # Special post-deployment for ingress-nginx namespace: label for NetworkPolicy + if [[ "$ns" == "ingress-nginx" ]]; then + if kubectl label namespace "$ns" name=ingress-nginx --overwrite; then + log_success "Labeled namespace '$ns' with name=ingress-nginx for NetworkPolicy access" + else + log_error "Failed to label namespace '$ns' - NetworkPolicy may not work correctly" + fi fi # Post-deploy status logs From 6ef53e096263b9ccf3c84447177fa1f43c8bd4e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:25:09 +0000 Subject: [PATCH 10/14] fix: Add error handling for namespace labeling and fix error message wording Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- n8n/deploy.sh | 4 ++-- wordpress/deploy.sh | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/n8n/deploy.sh b/n8n/deploy.sh index 1322e65..dba3955 100755 --- a/n8n/deploy.sh +++ b/n8n/deploy.sh @@ -246,7 +246,7 @@ install_ingress_nginx() { return 0 fi - log_info "NGINX Ingress Controller not found. Please run './cli/weown' and select the ingress-nginx stack to install shared ingress-nginx before deploying n8n." + log_info "NGINX Ingress Controller not found. Please run './cli/weown' and select 'Infra: Nginx Ingress' to install shared ingress-nginx before deploying n8n." exit 1 } @@ -260,7 +260,7 @@ install_cert_manager() { return 0 fi - log_info "cert-manager / ClusterIssuer not detected. Please run './cli/weown' and select the cert-manager stack to install shared cert-manager before deploying n8n." + log_info "cert-manager / ClusterIssuer not detected. Please run './cli/weown' and select 'Infra: Cert-Manager' to install shared cert-manager before deploying n8n." exit 1 } diff --git a/wordpress/deploy.sh b/wordpress/deploy.sh index 9c40447..967caf7 100755 --- a/wordpress/deploy.sh +++ b/wordpress/deploy.sh @@ -601,8 +601,11 @@ setup_infrastructure() { # Ensure ingress-nginx namespace has the required label for WordPress NetworkPolicy if ! kubectl get namespace ingress-nginx --show-labels | grep -q "name=ingress-nginx"; then log_substep "Adding required NetworkPolicy label to ingress-nginx namespace for shared ingress..." - kubectl label namespace ingress-nginx name=ingress-nginx --overwrite - log_substep "✓ NetworkPolicy label added to ingress-nginx namespace" + if kubectl label namespace ingress-nginx name=ingress-nginx --overwrite; then + log_substep "✓ NetworkPolicy label added to ingress-nginx namespace" + else + log_error "Failed to label ingress-nginx namespace - NetworkPolicy may not work correctly" + fi else log_substep "✓ ingress-nginx namespace already labeled for NetworkPolicy access" fi From 0cd03cb55613496bd0e683f624dca0d24410985e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:20:41 +0000 Subject: [PATCH 11/14] fix: Use jsonpath for label checks, strip CRLF, use --set-string for passwords Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- cli/lib/do_k8s.sh | 3 +++ cli/lib/stacks.sh | 8 +++++--- wordpress/deploy.sh | 6 ++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cli/lib/do_k8s.sh b/cli/lib/do_k8s.sh index 755d755..0f18068 100644 --- a/cli/lib/do_k8s.sh +++ b/cli/lib/do_k8s.sh @@ -33,6 +33,9 @@ if [ -n "$ENV_FILE" ]; then [A-Za-z_][A-Za-z0-9_]*=*) key=${line%%=*} value=${line#*=} + + # Strip trailing carriage return (Windows CRLF line endings) + value="${value%$'\r'}" # Strip matching surrounding quotes (both double or both single) # Only strip if quote appears at both start AND end diff --git a/cli/lib/stacks.sh b/cli/lib/stacks.sh index 83f4d4e..07a6272 100644 --- a/cli/lib/stacks.sh +++ b/cli/lib/stacks.sh @@ -92,7 +92,8 @@ install_selection() { fi # Populate chart values overriding placeholders / empty hosts extra_args_array+=(--set "wordpress.domain=${wp_domain}") - extra_args_array+=(--set "wordpress.wordpressPassword=${wp_admin_password}") + # Use --set-string for password to avoid Helm parsing issues with commas/booleans/numbers + extra_args_array+=(--set-string "wordpress.wordpressPassword=${wp_admin_password}") extra_args_array+=(--set "ingress.hosts[0].host=${wp_domain}") extra_args_array+=(--set "ingress.tls[0].hosts[0]=${wp_domain}") @@ -170,8 +171,9 @@ install_selection() { extra_args_array+=(--set "global.email=${LETSENCRYPT_EMAIL}") extra_args_array+=(--set "matomo.admin.email=${LETSENCRYPT_EMAIL}") fi - extra_args_array+=(--set "mariadb.auth.rootPassword=${matomo_db_root_password}") - extra_args_array+=(--set "mariadb.auth.password=${matomo_db_password}") + # Use --set-string for passwords to avoid Helm parsing issues with commas/booleans/numbers + extra_args_array+=(--set-string "mariadb.auth.rootPassword=${matomo_db_root_password}") + extra_args_array+=(--set-string "mariadb.auth.password=${matomo_db_password}") extra_args_array+=(--set "ingress.hosts[0].host=${matomo_domain}") extra_args_array+=(--set "ingress.tls[0].hosts[0]=${matomo_domain}") if [ -n "$wp_tracking_host" ]; then diff --git a/wordpress/deploy.sh b/wordpress/deploy.sh index 967caf7..f2d43b5 100755 --- a/wordpress/deploy.sh +++ b/wordpress/deploy.sh @@ -599,7 +599,8 @@ setup_infrastructure() { if kubectl get svc ingress-nginx-controller -n ingress-nginx >/dev/null 2>&1; then # Ensure ingress-nginx namespace has the required label for WordPress NetworkPolicy - if ! kubectl get namespace ingress-nginx --show-labels | grep -q "name=ingress-nginx"; then + # Use jsonpath to check for exact label key to avoid false matches with kubernetes.io/metadata.name + if ! kubectl get namespace ingress-nginx -o jsonpath='{.metadata.labels.name}' 2>/dev/null | grep -q "^ingress-nginx$"; then log_substep "Adding required NetworkPolicy label to ingress-nginx namespace for shared ingress..." if kubectl label namespace ingress-nginx name=ingress-nginx --overwrite; then log_substep "✓ NetworkPolicy label added to ingress-nginx namespace" @@ -660,7 +661,8 @@ setup_infrastructure() { fi # CRITICAL: Ensure ingress-nginx namespace has the required label for NetworkPolicy - if ! kubectl get namespace ingress-nginx --show-labels | grep -q "name=ingress-nginx"; then + # Use jsonpath to check for exact label key to avoid false matches with kubernetes.io/metadata.name + if ! kubectl get namespace ingress-nginx -o jsonpath='{.metadata.labels.name}' 2>/dev/null | grep -q "^ingress-nginx$"; then log_substep "Adding required NetworkPolicy label to ingress-nginx namespace..." kubectl label namespace ingress-nginx name=ingress-nginx --overwrite log_substep "✓ NetworkPolicy label added" From 5a12917767def40a302b3ab08ce6e5b2bd7c44d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 03:22:57 +0000 Subject: [PATCH 12/14] fix: Add return codes for error propagation and fix error handling in kubectl label Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> --- cli/lib/do_k8s.sh | 2 +- cli/lib/stacks.sh | 2 ++ wordpress/deploy.sh | 8 ++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cli/lib/do_k8s.sh b/cli/lib/do_k8s.sh index 0f18068..c467a6c 100644 --- a/cli/lib/do_k8s.sh +++ b/cli/lib/do_k8s.sh @@ -132,7 +132,7 @@ create_node_pool() { local tags=$5 # Optional setup as "role=value" if [ -z "$pool_name" ] || [ -z "$size" ] || [ -z "$count" ]; then - log_error "Usage: create_node_pool [label]" + log_error "Usage: create_node_pool [cluster_name] [label]" return 1 fi diff --git a/cli/lib/stacks.sh b/cli/lib/stacks.sh index 07a6272..2b0b548 100644 --- a/cli/lib/stacks.sh +++ b/cli/lib/stacks.sh @@ -247,7 +247,9 @@ install_selection() { --format Name,Status,Region,Version,NodePools --no-header \ || log_warn "doctl cluster get failed for $CLUSTER_NAME" fi + return 0 else log_error "$display_name Installation Failed." + return 1 fi } diff --git a/wordpress/deploy.sh b/wordpress/deploy.sh index f2d43b5..295b84d 100755 --- a/wordpress/deploy.sh +++ b/wordpress/deploy.sh @@ -664,8 +664,12 @@ setup_infrastructure() { # Use jsonpath to check for exact label key to avoid false matches with kubernetes.io/metadata.name if ! kubectl get namespace ingress-nginx -o jsonpath='{.metadata.labels.name}' 2>/dev/null | grep -q "^ingress-nginx$"; then log_substep "Adding required NetworkPolicy label to ingress-nginx namespace..." - kubectl label namespace ingress-nginx name=ingress-nginx --overwrite - log_substep "✓ NetworkPolicy label added" + if kubectl label namespace ingress-nginx name=ingress-nginx --overwrite; then + log_substep "✓ NetworkPolicy label added" + else + log_error "Failed to label ingress-nginx namespace - NetworkPolicy may not work correctly" + return 1 + fi else log_substep "✓ NetworkPolicy label already present" fi From b61c232e685463c4a47245618114d9d300abd733 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:46:14 +0000 Subject: [PATCH 13/14] fix: Treat label failure as hard error, clarify usage docs, make error messages self-contained Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> Agent-Logs-Url: https://github.com/WeOwnNetwork/ai/sessions/bd4cfc78-078d-491c-b3cc-6fcde72c9f75 --- cli/lib/do_k8s.sh | 4 +++- cli/lib/stacks.sh | 13 ++++++++++++- wordpress/deploy.sh | 8 ++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/cli/lib/do_k8s.sh b/cli/lib/do_k8s.sh index c467a6c..0340d94 100644 --- a/cli/lib/do_k8s.sh +++ b/cli/lib/do_k8s.sh @@ -125,6 +125,7 @@ scale_node_pool() { } create_node_pool() { + # First arg is always cluster_name (with default), remaining args are positional local cluster_name=${1:-${CLUSTER_NAME:-weown-cluster}} local pool_name=$2 local size=$3 @@ -132,7 +133,8 @@ create_node_pool() { local tags=$5 # Optional setup as "role=value" if [ -z "$pool_name" ] || [ -z "$size" ] || [ -z "$count" ]; then - log_error "Usage: create_node_pool [cluster_name] [label]" + log_error "Usage: create_node_pool [label]" + log_error " (cluster_name defaults to \$CLUSTER_NAME or 'weown-cluster' if not provided)" return 1 fi diff --git a/cli/lib/stacks.sh b/cli/lib/stacks.sh index 2b0b548..a738897 100644 --- a/cli/lib/stacks.sh +++ b/cli/lib/stacks.sh @@ -64,7 +64,18 @@ install_selection() { # External DNS needs DigitalOcean token stored in a Kubernetes Secret if [[ "$rn" == "external-dns" ]]; then if [ -z "${DO_TOKEN_SECRET_NAME:-}" ]; then - log_error "ExternalDNS deployment requires DO_TOKEN_SECRET_NAME to be set to the name of a Kubernetes Secret containing the DigitalOcean API token under the 'digitalocean_api_token' key. Create this Secret securely using an env file (no --from-literal); see the docs for the exact kubectl command." + log_error "ExternalDNS deployment requires DO_TOKEN_SECRET_NAME environment variable." + log_error "This should be the name of a Kubernetes Secret containing your DigitalOcean API token." + log_error "Expected Secret key: 'digitalocean_api_token'" + log_error "" + log_error "Create the Secret securely using:" + log_error " kubectl create secret generic --from-literal=digitalocean_api_token= -n external-dns" + log_error "Or use an env file (recommended):" + log_error " echo 'digitalocean_api_token=' > /tmp/do-token.env" + log_error " kubectl create secret generic --from-env-file=/tmp/do-token.env -n external-dns" + log_error " rm /tmp/do-token.env" + log_error "" + log_error "Then set: export DO_TOKEN_SECRET_NAME=" return 1 fi extra_args_array+=(--set provider=digitalocean) diff --git a/wordpress/deploy.sh b/wordpress/deploy.sh index 295b84d..4e06a0d 100755 --- a/wordpress/deploy.sh +++ b/wordpress/deploy.sh @@ -602,11 +602,11 @@ setup_infrastructure() { # Use jsonpath to check for exact label key to avoid false matches with kubernetes.io/metadata.name if ! kubectl get namespace ingress-nginx -o jsonpath='{.metadata.labels.name}' 2>/dev/null | grep -q "^ingress-nginx$"; then log_substep "Adding required NetworkPolicy label to ingress-nginx namespace for shared ingress..." - if kubectl label namespace ingress-nginx name=ingress-nginx --overwrite; then - log_substep "✓ NetworkPolicy label added to ingress-nginx namespace" - else - log_error "Failed to label ingress-nginx namespace - NetworkPolicy may not work correctly" + if ! kubectl label namespace ingress-nginx name=ingress-nginx --overwrite; then + log_error "Failed to label ingress-nginx namespace - NetworkPolicy will not work correctly" + return 1 fi + log_substep "✓ NetworkPolicy label added to ingress-nginx namespace" else log_substep "✓ ingress-nginx namespace already labeled for NetworkPolicy access" fi From 3656bed6ebe53fa868d9fadac5107323b100bd80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:00:24 +0000 Subject: [PATCH 14/14] fix: Use secure secret creation methods, treat label failure as hard error, make cluster_name truly optional Co-authored-by: romandidomizio <123122767+romandidomizio@users.noreply.github.com> Agent-Logs-Url: https://github.com/WeOwnNetwork/ai/sessions/2f54cc7d-fa5f-466f-92b9-2045c4da7432 --- cli/lib/do_k8s.sh | 42 ++++++++++++++++++++++++++++++++++-------- cli/lib/stacks.sh | 25 +++++++++++++++---------- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/cli/lib/do_k8s.sh b/cli/lib/do_k8s.sh index 0340d94..deea9eb 100644 --- a/cli/lib/do_k8s.sh +++ b/cli/lib/do_k8s.sh @@ -125,16 +125,42 @@ scale_node_pool() { } create_node_pool() { - # First arg is always cluster_name (with default), remaining args are positional - local cluster_name=${1:-${CLUSTER_NAME:-weown-cluster}} - local pool_name=$2 - local size=$3 - local count=$4 - local tags=$5 # Optional setup as "role=value" + # cluster_name is optional: + # - 3 args => (cluster_name from $CLUSTER_NAME or 'weown-cluster') + # - 4+ args => [label] + local cluster_name + local pool_name + local size + local count + local tags # Optional setup as "role=value" + + if [ "$#" -eq 3 ]; then + # Form: create_node_pool + cluster_name=${CLUSTER_NAME:-weown-cluster} + pool_name=$1 + size=$2 + count=$3 + tags= + elif [ "$#" -ge 4 ]; then + # Form: create_node_pool [label] + cluster_name=${1:-${CLUSTER_NAME:-weown-cluster}} + pool_name=$2 + size=$3 + count=$4 + tags=$5 + else + log_error "Usage:" + log_error " create_node_pool " + log_error " create_node_pool [label]" + log_error " (cluster_name defaults to \$CLUSTER_NAME or 'weown-cluster' if not provided)" + return 1 + fi if [ -z "$pool_name" ] || [ -z "$size" ] || [ -z "$count" ]; then - log_error "Usage: create_node_pool [label]" - log_error " (cluster_name defaults to \$CLUSTER_NAME or 'weown-cluster' if not provided)" + log_error "Usage:" + log_error " create_node_pool " + log_error " create_node_pool [label]" + log_error " (cluster_name defaults to \$CLUSTER_NAME or 'weown-cluster' if not provided)" return 1 fi diff --git a/cli/lib/stacks.sh b/cli/lib/stacks.sh index a738897..ec69e0c 100644 --- a/cli/lib/stacks.sh +++ b/cli/lib/stacks.sh @@ -68,12 +68,17 @@ install_selection() { log_error "This should be the name of a Kubernetes Secret containing your DigitalOcean API token." log_error "Expected Secret key: 'digitalocean_api_token'" log_error "" - log_error "Create the Secret securely using:" - log_error " kubectl create secret generic --from-literal=digitalocean_api_token= -n external-dns" - log_error "Or use an env file (recommended):" - log_error " echo 'digitalocean_api_token=' > /tmp/do-token.env" - log_error " kubectl create secret generic --from-env-file=/tmp/do-token.env -n external-dns" - log_error " rm /tmp/do-token.env" + log_error "Create the Secret securely using a temporary env file:" + log_error " AUTH_FILE=\"\$(mktemp)\"" + log_error " trap 'rm -f \"\$AUTH_FILE\"' EXIT" + log_error " cat > \"\$AUTH_FILE\" << 'EOF'" + log_error "digitalocean_api_token=" + log_error "EOF" + log_error " kubectl create secret generic --from-env-file=\"\$AUTH_FILE\" -n external-dns" + log_error "" + log_error "Alternatively, pipe the value from stdin without writing to disk:" + log_error " printf 'digitalocean_api_token=' | tr -d '\\n'; read -s TOKEN; echo; \\" + log_error " kubectl create secret generic --from-env-file=<(printf 'digitalocean_api_token=%s\\n' \"\$TOKEN\") -n external-dns" log_error "" log_error "Then set: export DO_TOKEN_SECRET_NAME=" return 1 @@ -235,11 +240,11 @@ install_selection() { # Special post-deployment for ingress-nginx namespace: label for NetworkPolicy if [[ "$ns" == "ingress-nginx" ]]; then - if kubectl label namespace "$ns" name=ingress-nginx --overwrite; then - log_success "Labeled namespace '$ns' with name=ingress-nginx for NetworkPolicy access" - else - log_error "Failed to label namespace '$ns' - NetworkPolicy may not work correctly" + if ! kubectl label namespace "$ns" name=ingress-nginx --overwrite; then + log_error "Failed to label namespace '$ns' - NetworkPolicy will not work correctly; treating as hard failure" + return 1 fi + log_success "Labeled namespace '$ns' with name=ingress-nginx for NetworkPolicy access" fi # Post-deploy status logs