diff --git a/scripts/deploy/backup.sh b/scripts/deploy/backup.sh new file mode 100755 index 00000000..6fe62645 --- /dev/null +++ b/scripts/deploy/backup.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# backup.sh - PostgreSQL backup script for EnvSync K3S deployment +# Dumps the database to local disk and prunes backups older than 7 days +set -euo pipefail + +# Timestamp used for the backup folder name e.g. 2026-03-23_11-55-00 +DATE=$(date +%Y-%m-%d_%H-%M-%S) + +# Full path where this backup will be stored +BACKUP_DIR="/var/backups/envsync/${DATE}" + +# Kubernetes namespace where EnvSync pods are running +NAMESPACE="envsync" + +# Dynamically find the postgres pod name — never hardcode pod names, +# Kubernetes regenerates them on every restart +POSTGRES_POD=$(kubectl get pod -n "$NAMESPACE" \ + -l app=postgres \ + --no-headers \ + -o custom-columns=":metadata.name") + + echo "Starting EnvSync backup: ${DATE}" + +# -p creates parent directories if they don't exist +# and won't error if the folder already exists +mkdir -p "${BACKUP_DIR}" + +echo "Dumping PostgreSQL..." + +# kubectl exec runs a command inside the running postgres pod +# The -- separates kubectl flags from the command being run inside the pod +# Output is redirected into a .sql file in our backup directory +kubectl exec -n "$NAMESPACE" "$POSTGRES_POD" -- \ + pg_dump -U postgres envsync \ + > "${BACKUP_DIR}/envsync_db.sql" + +echo "Pruning backups older than 7 days..." + +# find options explained: +# -maxdepth 1 only look one level deep, don't recurse into backup folders +# -type d only match directories, not files +# -mtime +7 only match directories older than 7 days +# -exec rm -rf {} + delete each match +find /var/backups/envsync -maxdepth 1 -type d -mtime +7 -exec rm -rf {} + + +echo "Backup complete: ${BACKUP_DIR}" + +# Show what was saved with human-readable file sizes +ls -lh "${BACKUP_DIR}" diff --git a/scripts/deploy/init.sh b/scripts/deploy/init.sh new file mode 100755 index 00000000..3532817e --- /dev/null +++ b/scripts/deploy/init.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# init.sh - Initializes a fresh Linux server with K3S and deploys EnvSync +# Platform agnostic - works on any Ubuntu/Debian server +# Usage: sudo bash init.sh +set -euo pipefail + +# Ask the operator for the root domain at runtime +# All subdomains will be generated from this e.g. api.envsync.cloud +echo "============================================" +echo " EnvSync K3S Deployment Setup " +echo "============================================" +read -rp "Enter your root domain (e.g. envsync.cloud): " ROOT_DOMAIN + +# Confirm before proceeding +echo "" +echo "Deploying EnvSync to the following subdomains:" +echo " API: api.${ROOT_DOMAIN}" +echo " Auth: auth.${ROOT_DOMAIN}" +echo " S3: s3.${ROOT_DOMAIN}" +echo " Web app: app.${ROOT_DOMAIN}" +echo "" +read -rp "Continue? (y/n): " CONFIRM +if [[ "$CONFIRM" != "y" ]]; then + echo "Aborted." + exit 0 +fi + +echo "Installing K3S..." + +# K3S is a lightweight single-node Kubernetes distribution +# --write-kubeconfig-mode 644 makes the kubeconfig readable without sudo +curl -sfL https://get.k3s.io | sh -s - \ + --write-kubeconfig-mode 644 + +echo "Waiting for K3S to be ready..." + +# Loop until the node reports Ready status +# This usually takes 20-30 seconds on first boot +until kubectl get node | grep -q "Ready"; do + echo " ...waiting for node" + sleep 5 +done + +echo "K3S is ready." + +# Directory where our manifests live relative to this script +MANIFEST_DIR="$(dirname "$0")/manifests" + +echo "Injecting domain into manifests..." + +# sed replaces the PLACEHOLDER values in the manifest files +# with the actual domain the operator entered +# -i edits the files in place +sed -i "s/PLACEHOLDER_API_DOMAIN/api.${ROOT_DOMAIN}/g" "$MANIFEST_DIR/13-ingress.yaml" +sed -i "s/PLACEHOLDER_APP_DOMAIN/app.${ROOT_DOMAIN}/g" "$MANIFEST_DIR/13-ingress.yaml" +sed -i "s/PLACEHOLDER_AUTH_DOMAIN/auth.${ROOT_DOMAIN}/g" "$MANIFEST_DIR/13-ingress.yaml" +sed -i "s/PLACEHOLDER_S3_DOMAIN/s3.${ROOT_DOMAIN}/g" "$MANIFEST_DIR/13-ingress.yaml" + +# Also inject the auth domain into zitadel so it knows its external URL +sed -i "s/PLACEHOLDER_AUTH_DOMAIN/auth.${ROOT_DOMAIN}/g" "$MANIFEST_DIR/10-zitadel.yaml" + +# Also inject the api domain into envsync-web so it knows where the API is +sed -i "s/PLACEHOLDER_API_DOMAIN/api.${ROOT_DOMAIN}/g" "$MANIFEST_DIR/12-envsync-web.yaml" + +echo "Applying manifests in dependency order..." + +# Apply each manifest in numbered order +# Each number prefix ensures correct dependency sequencing +for manifest in "$MANIFEST_DIR"/*.yaml; do + echo " Applying $(basename "$manifest")..." + kubectl apply -f "$manifest" +done + +echo "Waiting for core pods to be ready..." + +# Wait for postgres first since everything depends on it +kubectl wait --namespace envsync \ + --for=condition=ready pod \ + --selector=app=postgres \ + --timeout=120s + +# Wait for envsync-api last since it depends on all other services +kubectl wait --namespace envsync \ + --for=condition=ready pod \ + --selector=app=envsync-api \ + --timeout=180s + +echo "Pods are ready." + +# Run EnvSync's own initialization sequence +# This creates Zitadel OIDC apps and writes client IDs to .env +echo "Running EnvSync init..." +bun run cli init + +echo "" +echo "============================================" +echo " EnvSync deployed successfully!" +echo "============================================" +echo " API: https://api.${ROOT_DOMAIN}" +echo " Auth: https://auth.${ROOT_DOMAIN}" +echo " S3: https://s3.${ROOT_DOMAIN}" +echo " Web app: https://app.${ROOT_DOMAIN}" +echo "============================================" \ No newline at end of file diff --git a/scripts/deploy/manifests/00-secrets.yaml b/scripts/deploy/manifests/00-secrets.yaml new file mode 100644 index 00000000..e019b8e6 --- /dev/null +++ b/scripts/deploy/manifests/00-secrets.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: envsync-secrets + namespace: envsync +type: Opaque +stringData: + MINIKMS_ROOT_KEY: "changeme-replace-with-real-32-char-key" \ No newline at end of file diff --git a/scripts/deploy/manifests/01-namespace.yaml b/scripts/deploy/manifests/01-namespace.yaml new file mode 100644 index 00000000..7732298c --- /dev/null +++ b/scripts/deploy/manifests/01-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: envsync \ No newline at end of file diff --git a/scripts/deploy/manifests/02-postgres.yaml b/scripts/deploy/manifests/02-postgres.yaml new file mode 100644 index 00000000..919ae7e2 --- /dev/null +++ b/scripts/deploy/manifests/02-postgres.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:17 + env: + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: postgres + - name: POSTGRES_DB + value: envsync + ports: + - containerPort: 5432 +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: envsync +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 \ No newline at end of file diff --git a/scripts/deploy/manifests/03-redis.yaml b/scripts/deploy/manifests/03-redis.yaml new file mode 100644 index 00000000..a9d905ed --- /dev/null +++ b/scripts/deploy/manifests/03-redis.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:latest + ports: + - containerPort: 6379 +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: envsync +spec: + selector: + app: redis + ports: + - port: 6379 + targetPort: 6379 \ No newline at end of file diff --git a/scripts/deploy/manifests/04-zitadel-db.yaml b/scripts/deploy/manifests/04-zitadel-db.yaml new file mode 100644 index 00000000..b8d379e3 --- /dev/null +++ b/scripts/deploy/manifests/04-zitadel-db.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: zitadel-db + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: zitadel-db + template: + metadata: + labels: + app: zitadel-db + spec: + containers: + - name: zitadel-db + image: postgres:17 + env: + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: postgres + - name: POSTGRES_DB + value: zitadel + ports: + - containerPort: 5432 +--- +apiVersion: v1 +kind: Service +metadata: + name: zitadel-db + namespace: envsync +spec: + selector: + app: zitadel-db + ports: + - port: 5432 + targetPort: 5432 \ No newline at end of file diff --git a/scripts/deploy/manifests/05-minikms-db.yaml b/scripts/deploy/manifests/05-minikms-db.yaml new file mode 100644 index 00000000..80bd1e77 --- /dev/null +++ b/scripts/deploy/manifests/05-minikms-db.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: minikms-db + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: minikms-db + template: + metadata: + labels: + app: minikms-db + spec: + containers: + - name: minikms-db + image: postgres:17 + env: + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: postgres + - name: POSTGRES_DB + value: minikms + ports: + - containerPort: 5432 +--- +apiVersion: v1 +kind: Service +metadata: + name: minikms-db + namespace: envsync +spec: + selector: + app: minikms-db + ports: + - port: 5432 + targetPort: 5432 \ No newline at end of file diff --git a/scripts/deploy/manifests/06-openfga-db.yaml b/scripts/deploy/manifests/06-openfga-db.yaml new file mode 100644 index 00000000..9e9cfb9f --- /dev/null +++ b/scripts/deploy/manifests/06-openfga-db.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: openfga-db + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: openfga-db + template: + metadata: + labels: + app: openfga-db + spec: + containers: + - name: openfga-db + image: postgres:17 + env: + - name: POSTGRES_USER + value: openfga + - name: POSTGRES_PASSWORD + value: openfga + - name: POSTGRES_DB + value: openfga + ports: + - containerPort: 5432 +--- +apiVersion: v1 +kind: Service +metadata: + name: openfga-db + namespace: envsync +spec: + selector: + app: openfga-db + ports: + - port: 5432 + targetPort: 5432 \ No newline at end of file diff --git a/scripts/deploy/manifests/07-rustfs.yaml b/scripts/deploy/manifests/07-rustfs.yaml new file mode 100644 index 00000000..51ecff4b --- /dev/null +++ b/scripts/deploy/manifests/07-rustfs.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rustfs + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: rustfs + template: + metadata: + labels: + app: rustfs + spec: + containers: + - name: rustfs + image: rustfs/rustfs:latest + env: + - name: RUSTFS_DATA_DIR + value: /data + - name: RUSTFS_ACCESS_KEY + value: rustfsadmin + - name: RUSTFS_SECRET_KEY + value: rustfsadmin + - name: RUSTFS_CONSOLE_ENABLE + value: "false" + ports: + - containerPort: 9000 +--- +apiVersion: v1 +kind: Service +metadata: + name: rustfs + namespace: envsync +spec: + selector: + app: rustfs + ports: + - port: 9000 + targetPort: 9000 \ No newline at end of file diff --git a/scripts/deploy/manifests/08-minikms.yaml b/scripts/deploy/manifests/08-minikms.yaml new file mode 100644 index 00000000..c28a36c6 --- /dev/null +++ b/scripts/deploy/manifests/08-minikms.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: minikms + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: minikms + template: + metadata: + labels: + app: minikms + spec: + containers: + - name: minikms + image: ghcr.io/envsync-cloud/minikms:sha-735dfe8 + env: + - name: MINIKMS_ROOT_KEY + valueFrom: + secretKeyRef: + name: envsync-secrets + key: MINIKMS_ROOT_KEY + - name: MINIKMS_DB_URL + value: postgres://postgres:postgres@minikms-db:5432/minikms?sslmode=disable + - name: MINIKMS_REDIS_URL + value: redis://redis:6379 + - name: MINIKMS_TLS_ENABLED + value: "false" + ports: + - containerPort: 50051 +--- +apiVersion: v1 +kind: Service +metadata: + name: minikms + namespace: envsync +spec: + selector: + app: minikms + ports: + - port: 50051 + targetPort: 50051 \ No newline at end of file diff --git a/scripts/deploy/manifests/09-openfga.yaml b/scripts/deploy/manifests/09-openfga.yaml new file mode 100644 index 00000000..b5440346 --- /dev/null +++ b/scripts/deploy/manifests/09-openfga.yaml @@ -0,0 +1,68 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: openfga-migrate + namespace: envsync +spec: + template: + spec: + containers: + - name: openfga-migrate + image: openfga/openfga:v1.12.0 + command: ["./openfga", "migrate"] + env: + - name: OPENFGA_DATASTORE_ENGINE + value: postgres + - name: OPENFGA_DATASTORE_URI + value: postgres://openfga:openfga@openfga-db:5432/openfga?sslmode=disable + restartPolicy: OnFailure +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: openfga + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: openfga + template: + metadata: + labels: + app: openfga + spec: + containers: + - name: openfga + image: openfga/openfga:v1.12.0 + command: ["./openfga", "run"] + env: + - name: OPENFGA_DATASTORE_ENGINE + value: postgres + - name: OPENFGA_DATASTORE_URI + value: postgres://openfga:openfga@openfga-db:5432/openfga?sslmode=disable + - name: OPENFGA_PLAYGROUND_ENABLED + value: "false" + - name: OPENFGA_HTTP_ADDR + value: 0.0.0.0:8090 + - name: OPENFGA_GRPC_ADDR + value: 0.0.0.0:8091 + ports: + - containerPort: 8090 + - containerPort: 8091 +--- +apiVersion: v1 +kind: Service +metadata: + name: openfga + namespace: envsync +spec: + selector: + app: openfga + ports: + - name: http + port: 8090 + targetPort: 8090 + - name: grpc + port: 8091 + targetPort: 8091 \ No newline at end of file diff --git a/scripts/deploy/manifests/10-zitadel.yaml b/scripts/deploy/manifests/10-zitadel.yaml new file mode 100644 index 00000000..c409ea8c --- /dev/null +++ b/scripts/deploy/manifests/10-zitadel.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: zitadel + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: zitadel + template: + metadata: + labels: + app: zitadel + spec: + containers: + - name: zitadel + image: ghcr.io/zitadel/zitadel:latest + args: + - start-from-init + - --masterkey + - MasterkeyNeedsToHave32Characters + env: + - name: ZITADEL_EXTERNALDOMAIN + value: PLACEHOLDER_AUTH_DOMAIN + - name: ZITADEL_EXTERNALSECURE + value: "true" + - name: ZITADEL_TLS_ENABLED + value: "false" + - name: ZITADEL_DATABASE_POSTGRES_HOST + value: zitadel-db + - name: ZITADEL_DATABASE_POSTGRES_PORT + value: "5432" + - name: ZITADEL_DATABASE_POSTGRES_DATABASE + value: zitadel + - name: ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME + value: postgres + - name: ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD + value: postgres + - name: ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE + value: disable + - name: ZITADEL_DATABASE_POSTGRES_USER_USERNAME + value: zitadel + - name: ZITADEL_DATABASE_POSTGRES_USER_PASSWORD + value: zitadel + - name: ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE + value: disable + - name: ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME + value: zitadel-admin + - name: ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD + value: Password1! + - name: ZITADEL_FIRSTINSTANCE_ORG_NAME + value: EnvSync + - name: ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED + value: "false" + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: zitadel + namespace: envsync +spec: + selector: + app: zitadel + ports: + - port: 8080 + targetPort: 8080 \ No newline at end of file diff --git a/scripts/deploy/manifests/11-envsync-api.yaml b/scripts/deploy/manifests/11-envsync-api.yaml new file mode 100644 index 00000000..5709ce69 --- /dev/null +++ b/scripts/deploy/manifests/11-envsync-api.yaml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: envsync-api + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: envsync-api + template: + metadata: + labels: + app: envsync-api + spec: + containers: + - name: envsync-api + image: ghcr.io/envsync-cloud/envsync-api:latest + env: + - name: NODE_ENV + value: production + - name: DATABASE_HOST + value: postgres + - name: REDIS_URL + value: redis://redis:6379 + - name: S3_ENDPOINT + value: http://rustfs:9000 + - name: S3_BUCKET_URL + value: http://rustfs:9000 + - name: OPENFGA_API_URL + value: http://openfga:8090 + - name: ZITADEL_URL + value: http://zitadel:8080 + - name: MINIKMS_GRPC_ADDR + value: minikms:50051 + ports: + - containerPort: 4000 +--- +apiVersion: v1 +kind: Service +metadata: + name: envsync-api + namespace: envsync +spec: + selector: + app: envsync-api + ports: + - port: 4000 + targetPort: 4000 \ No newline at end of file diff --git a/scripts/deploy/manifests/12-envsync-web.yaml b/scripts/deploy/manifests/12-envsync-web.yaml new file mode 100644 index 00000000..270c50bd --- /dev/null +++ b/scripts/deploy/manifests/12-envsync-web.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: envsync-web + namespace: envsync +spec: + replicas: 1 + selector: + matchLabels: + app: envsync-web + template: + metadata: + labels: + app: envsync-web + spec: + containers: + - name: envsync-web + image: ghcr.io/envsync-cloud/envsync-web:latest + env: + - name: VITE_API_BASE_URL + value: PLACEHOLDER_API_DOMAIN + ports: + - containerPort: 5173 +--- +apiVersion: v1 +kind: Service +metadata: + name: envsync-web + namespace: envsync +spec: + selector: + app: envsync-web + ports: + - port: 5173 + targetPort: 5173 \ No newline at end of file diff --git a/scripts/deploy/manifests/13-ingress.yaml b/scripts/deploy/manifests/13-ingress.yaml new file mode 100644 index 00000000..1a0440d1 --- /dev/null +++ b/scripts/deploy/manifests/13-ingress.yaml @@ -0,0 +1,59 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: envsync-api + namespace: envsync +spec: + entryPoints: + - websecure + routes: + - match: Host(`PLACEHOLDER_API_DOMAIN`) + kind: Rule + services: + - name: envsync-api + port: 4000 +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: envsync-web + namespace: envsync +spec: + entryPoints: + - websecure + routes: + - match: Host(`PLACEHOLDER_APP_DOMAIN`) + kind: Rule + services: + - name: envsync-web + port: 5173 +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: envsync-auth + namespace: envsync +spec: + entryPoints: + - websecure + routes: + - match: Host(`PLACEHOLDER_AUTH_DOMAIN`) + kind: Rule + services: + - name: zitadel + port: 8080 +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: envsync-s3 + namespace: envsync +spec: + entryPoints: + - websecure + routes: + - match: Host(`PLACEHOLDER_S3_DOMAIN`) + kind: Rule + services: + - name: rustfs + port: 9000 \ No newline at end of file diff --git a/scripts/deploy/upgrade.sh b/scripts/deploy/upgrade.sh new file mode 100755 index 00000000..b9e07e98 --- /dev/null +++ b/scripts/deploy/upgrade.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# upgrade.sh - Upgrades EnvSync services to a new image version on K3S +# Usage: sudo bash upgrade.sh +# Example: sudo bash upgrade.sh v0.4.2 +set -euo pipefail + +# Require exactly one argument — the new image tag to deploy +if [[ $# -ne 1 ]]; then + echo "Usage: bash upgrade.sh " + echo "Example: bash upgrade.sh v0.4.2" + exit 1 +fi + +NEW_TAG="$1" +NAMESPACE="envsync" + +echo "============================================" +echo " EnvSync Upgrade: ${NEW_TAG}" +echo "============================================" + +# Always back up before upgrading — if anything goes wrong we can restore +echo "Running pre-upgrade backup..." +bash "$(dirname "$0")/backup.sh" +echo "Backup complete. Proceeding with upgrade..." + +# Update each deployment to the new image tag +# kubectl set image triggers a rolling update — pods are replaced one by one +# so the service stays available during the upgrade +echo "Updating envsync-api..." +kubectl set image deployment/envsync-api \ + envsync-api=ghcr.io/envsync-cloud/envsync-api:${NEW_TAG} \ + -n "$NAMESPACE" + +echo "Updating envsync-web..." +kubectl set image deployment/envsync-web \ + envsync-web=ghcr.io/envsync-cloud/envsync-web:${NEW_TAG} \ + -n "$NAMESPACE" + +echo "Watching rollout status..." + +# kubectl rollout status blocks until the rollout completes or fails +# If it fails we immediately roll back to the previous working version +if ! kubectl rollout status deployment/envsync-api -n "$NAMESPACE" --timeout=120s; then + echo "Rollout failed — rolling back envsync-api..." + kubectl rollout undo deployment/envsync-api -n "$NAMESPACE" + exit 1 +fi + +if ! kubectl rollout status deployment/envsync-web -n "$NAMESPACE" --timeout=120s; then + echo "Rollout failed — rolling back envsync-web..." + kubectl rollout undo deployment/envsync-web -n "$NAMESPACE" + exit 1 +fi + +echo "" +echo "============================================" +echo " Upgrade to ${NEW_TAG} complete!" +echo "============================================" \ No newline at end of file