Add spec.paused: null to the strategic merge patch to clear any paused state that might be blocking rollouts. Also add detailed deployment status debug output (conditions, replica counts, paused state) and list ReplicaSets to diagnose why new pods aren't being created despite spec changes.
397 lines
16 KiB
YAML
397 lines
16 KiB
YAML
kind: pipeline
|
|
type: kubernetes
|
|
name: frontend
|
|
|
|
clone:
|
|
disable: true
|
|
|
|
concurrency:
|
|
limit: 1
|
|
|
|
timeout: 20
|
|
|
|
trigger:
|
|
branch:
|
|
- master
|
|
event:
|
|
- push
|
|
|
|
steps:
|
|
- name: clone
|
|
image: alpine/git
|
|
commands:
|
|
- |
|
|
for i in 1 2 3 4 5; do
|
|
git clone --depth=50 "$DRONE_REMOTE_URL" . && exit 0
|
|
echo "Clone attempt $i failed, retrying in 5s..."
|
|
sleep 5
|
|
done
|
|
echo "Clone failed after 5 attempts"
|
|
exit 1
|
|
- git checkout "$DRONE_COMMIT"
|
|
|
|
- name: install-frontend-deps
|
|
image: node:24-alpine
|
|
depends_on:
|
|
- clone
|
|
environment:
|
|
NODE_OPTIONS: "--max-old-space-size=1024"
|
|
commands:
|
|
- cd frontend && npm ci
|
|
|
|
- name: test-shard-1
|
|
image: node:24-alpine
|
|
depends_on:
|
|
- install-frontend-deps
|
|
environment:
|
|
NODE_OPTIONS: "--max-old-space-size=1024"
|
|
commands:
|
|
- cd frontend && npx vitest run --reporter=verbose --shard=1/4
|
|
|
|
- name: test-shard-2
|
|
image: node:24-alpine
|
|
depends_on:
|
|
- install-frontend-deps
|
|
environment:
|
|
NODE_OPTIONS: "--max-old-space-size=1024"
|
|
commands:
|
|
- cd frontend && npx vitest run --reporter=verbose --shard=2/4
|
|
|
|
- name: test-shard-3
|
|
image: node:24-alpine
|
|
depends_on:
|
|
- install-frontend-deps
|
|
environment:
|
|
NODE_OPTIONS: "--max-old-space-size=1024"
|
|
commands:
|
|
- cd frontend && npx vitest run --reporter=verbose --shard=3/4
|
|
|
|
- name: test-shard-4
|
|
image: node:24-alpine
|
|
depends_on:
|
|
- install-frontend-deps
|
|
environment:
|
|
NODE_OPTIONS: "--max-old-space-size=1024"
|
|
commands:
|
|
- cd frontend && npx vitest run --reporter=verbose --shard=4/4
|
|
|
|
- name: build-frontend-image
|
|
image: plugins/kaniko
|
|
depends_on:
|
|
- clone
|
|
resources:
|
|
limits:
|
|
memory: 2048MiB
|
|
settings:
|
|
username: viktorbarzin
|
|
password:
|
|
from_secret: dockerhub-token
|
|
repo: viktorbarzin/immoweb
|
|
dockerfile: frontend/Dockerfile
|
|
context: frontend
|
|
target: production
|
|
enable_cache: true
|
|
cache_repo: viktorbarzin/immoweb-cache
|
|
tags:
|
|
- "build-${DRONE_BUILD_NUMBER}"
|
|
|
|
- name: publish-frontend-image
|
|
image: alpine
|
|
depends_on:
|
|
- test-shard-1
|
|
- test-shard-2
|
|
- test-shard-3
|
|
- test-shard-4
|
|
- build-frontend-image
|
|
environment:
|
|
DOCKERHUB_TOKEN:
|
|
from_secret: dockerhub-token
|
|
commands:
|
|
- apk add --no-cache skopeo
|
|
- 'skopeo copy --src-creds "viktorbarzin:$DOCKERHUB_TOKEN" --dest-creds "viktorbarzin:$DOCKERHUB_TOKEN" "docker://docker.io/viktorbarzin/immoweb:build-${DRONE_BUILD_NUMBER}" "docker://docker.io/viktorbarzin/immoweb:${DRONE_BUILD_NUMBER}"'
|
|
- 'skopeo copy --src-creds "viktorbarzin:$DOCKERHUB_TOKEN" --dest-creds "viktorbarzin:$DOCKERHUB_TOKEN" "docker://docker.io/viktorbarzin/immoweb:build-${DRONE_BUILD_NUMBER}" "docker://docker.io/viktorbarzin/immoweb:latest"'
|
|
|
|
- name: Update deployment
|
|
image: alpine
|
|
depends_on:
|
|
- publish-frontend-image
|
|
commands:
|
|
- apk add --no-cache curl jq
|
|
- |
|
|
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
|
|
IMAGE="viktorbarzin/immoweb:${DRONE_BUILD_NUMBER}"
|
|
RESTART_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
API="https://kubernetes:6443/apis/apps/v1/namespaces/realestate-crawler/deployments"
|
|
DEPLOY="realestate-crawler-ui"
|
|
|
|
CONTAINER=$(curl -sfk "$API/$DEPLOY" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Accept: application/json" | jq -r '.spec.template.spec.containers[0].name')
|
|
echo "Patching $DEPLOY (container=$CONTAINER) to image $IMAGE with restartedAt=$RESTART_AT..."
|
|
|
|
curl -sf -X PATCH "$API/$DEPLOY" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Content-Type: application/strategic-merge-patch+json" \
|
|
-k -d "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"kubectl.kubernetes.io/restartedAt\":\"$RESTART_AT\"}},\"spec\":{\"containers\":[{\"name\":\"$CONTAINER\",\"image\":\"$IMAGE\"}]}}}}" \
|
|
| jq '{name: .metadata.name, generation: .metadata.generation, image: .spec.template.spec.containers[0].image}'
|
|
|
|
- name: verify-deploy
|
|
image: alpine
|
|
depends_on:
|
|
- Update deployment
|
|
commands:
|
|
- apk add --no-cache curl jq
|
|
- |
|
|
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
|
|
EXPECTED_IMAGE="viktorbarzin/immoweb:${DRONE_BUILD_NUMBER}"
|
|
PODS_API="https://kubernetes:6443/api/v1/namespaces/realestate-crawler/pods?labelSelector=app%3Drealestate-crawler-ui"
|
|
|
|
for i in $(seq 1 60); do
|
|
RAW=$(curl -sfk "$PODS_API" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Accept: application/json")
|
|
|
|
# Debug: show all pod images and status on first attempt
|
|
if [ "$i" -eq 1 ]; then
|
|
echo "DEBUG: All pods for realestate-crawler-ui:"
|
|
echo "$RAW" | jq -r '[.items[] | {name: .metadata.name, image: .spec.containers[0].image, ready: (.status.containerStatuses[]? | .ready), phase: .status.phase}] | .[] | " \(.name) image=\(.image) ready=\(.ready) phase=\(.phase)"' 2>/dev/null || echo " (no pods found)"
|
|
fi
|
|
|
|
RESULT=$(echo "$RAW" | \
|
|
jq --arg img "$EXPECTED_IMAGE" '[.items[] | select(
|
|
(.status.containerStatuses[]? | .ready == true) and
|
|
(.spec.containers[]? | .image | endswith($img))
|
|
) | {name: .metadata.name, image: .spec.containers[0].image, started: .status.startTime}]')
|
|
|
|
COUNT=$(echo "$RESULT" | jq 'length')
|
|
echo "Attempt $i/60: $COUNT pod(s) ready with image matching $EXPECTED_IMAGE"
|
|
|
|
if [ "$COUNT" -gt 0 ]; then
|
|
echo "$RESULT" | jq -r '.[] | " \(.name) image=\(.image) started=\(.started)"'
|
|
echo "New pod is live!"
|
|
exit 0
|
|
fi
|
|
|
|
sleep 5
|
|
done
|
|
|
|
echo "ERROR: No new ready pod with image $EXPECTED_IMAGE appeared within 5 minutes"
|
|
exit 1
|
|
|
|
---
|
|
kind: pipeline
|
|
type: kubernetes
|
|
name: api
|
|
|
|
clone:
|
|
disable: true
|
|
|
|
concurrency:
|
|
limit: 1
|
|
|
|
timeout: 20
|
|
|
|
trigger:
|
|
branch:
|
|
- master
|
|
event:
|
|
- push
|
|
|
|
steps:
|
|
- name: clone
|
|
image: alpine/git
|
|
commands:
|
|
- |
|
|
for i in 1 2 3 4 5; do
|
|
git clone --depth=50 "$DRONE_REMOTE_URL" . && exit 0
|
|
echo "Clone attempt $i failed, retrying in 5s..."
|
|
sleep 5
|
|
done
|
|
echo "Clone failed after 5 attempts"
|
|
exit 1
|
|
- git checkout "$DRONE_COMMIT"
|
|
|
|
- name: install-api-deps
|
|
image: python:3.13-slim
|
|
depends_on:
|
|
- clone
|
|
commands:
|
|
- apt-get update && apt-get install -y --no-install-recommends libglib2.0-0
|
|
- python -m venv .venv
|
|
- .venv/bin/pip install --quiet --upgrade pip
|
|
- >-
|
|
.venv/bin/pip install --quiet
|
|
pytest pytest-asyncio pytest-cov httpx fakeredis aioresponses
|
|
fastapi uvicorn sqlmodel sqlalchemy alembic pyjwt cryptography
|
|
celery redis click aiohttp aiohttp-socks pillow numpy pytesseract
|
|
opentelemetry-api opentelemetry-sdk opentelemetry-exporter-prometheus
|
|
opentelemetry-instrumentation-fastapi opentelemetry-instrumentation-sqlalchemy
|
|
python-dotenv webauthn apprise tenacity prometheus-client
|
|
email-validator opencv-python-headless tqdm pandas cachetools watchdog
|
|
|
|
- name: test-unit
|
|
image: python:3.13-slim
|
|
depends_on:
|
|
- install-api-deps
|
|
commands:
|
|
- apt-get update && apt-get install -y --no-install-recommends libglib2.0-0
|
|
- .venv/bin/pytest tests/unit/ -v --tb=short
|
|
|
|
- name: test-integration
|
|
image: python:3.13-slim
|
|
depends_on:
|
|
- install-api-deps
|
|
commands:
|
|
- apt-get update && apt-get install -y --no-install-recommends libglib2.0-0
|
|
- .venv/bin/pytest tests/integration/ tests/regression/ tests/e2e/ tests/test_listing_geojson.py -v --tb=short
|
|
|
|
- name: build-api-image
|
|
image: plugins/docker
|
|
depends_on:
|
|
- clone
|
|
environment:
|
|
DOCKER_BUILDKIT: 1
|
|
settings:
|
|
username: viktorbarzin
|
|
password:
|
|
from_secret: dockerhub-token
|
|
repo: viktorbarzin/realestatecrawler
|
|
dockerfile: Dockerfile
|
|
context: .
|
|
target: production
|
|
cache_from:
|
|
- viktorbarzin/realestatecrawler:latest
|
|
- viktorbarzin/realestatecrawler:builder
|
|
tags:
|
|
- "build-${DRONE_BUILD_NUMBER}"
|
|
|
|
- name: publish-api-image
|
|
image: alpine
|
|
depends_on:
|
|
- test-unit
|
|
- test-integration
|
|
- build-api-image
|
|
environment:
|
|
DOCKERHUB_TOKEN:
|
|
from_secret: dockerhub-token
|
|
commands:
|
|
- apk add --no-cache skopeo
|
|
- 'skopeo copy --src-creds "viktorbarzin:$DOCKERHUB_TOKEN" --dest-creds "viktorbarzin:$DOCKERHUB_TOKEN" "docker://docker.io/viktorbarzin/realestatecrawler:build-${DRONE_BUILD_NUMBER}" "docker://docker.io/viktorbarzin/realestatecrawler:${DRONE_BUILD_NUMBER}"'
|
|
- 'skopeo copy --src-creds "viktorbarzin:$DOCKERHUB_TOKEN" --dest-creds "viktorbarzin:$DOCKERHUB_TOKEN" "docker://docker.io/viktorbarzin/realestatecrawler:build-${DRONE_BUILD_NUMBER}" "docker://docker.io/viktorbarzin/realestatecrawler:latest"'
|
|
- 'skopeo copy --src-creds "viktorbarzin:$DOCKERHUB_TOKEN" --dest-creds "viktorbarzin:$DOCKERHUB_TOKEN" "docker://docker.io/viktorbarzin/realestatecrawler:build-${DRONE_BUILD_NUMBER}" "docker://docker.io/viktorbarzin/realestatecrawler:builder"'
|
|
|
|
- name: Update deployment
|
|
image: alpine
|
|
depends_on:
|
|
- publish-api-image
|
|
commands:
|
|
- apk add --no-cache curl jq
|
|
- |
|
|
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
|
|
IMAGE="viktorbarzin/realestatecrawler:${DRONE_BUILD_NUMBER}"
|
|
RESTART_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
API="https://kubernetes:6443/apis/apps/v1/namespaces/realestate-crawler/deployments"
|
|
|
|
for DEPLOY in realestate-crawler-api realestate-crawler-celery realestate-crawler-celery-beat; do
|
|
# Check if deployment is paused and get container name
|
|
STATUS=$(curl -sfk "$API/$DEPLOY" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Accept: application/json")
|
|
CONTAINER=$(echo "$STATUS" | jq -r '.spec.template.spec.containers[0].name')
|
|
PAUSED=$(echo "$STATUS" | jq -r '.spec.paused // false')
|
|
echo "Patching $DEPLOY (container=$CONTAINER, paused=$PAUSED) to image $IMAGE..."
|
|
|
|
# Strategic merge: update image, set restartedAt, ensure not paused
|
|
curl -sf -X PATCH "$API/$DEPLOY" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Content-Type: application/strategic-merge-patch+json" \
|
|
-k -d "{\"spec\":{\"paused\":null,\"template\":{\"metadata\":{\"annotations\":{\"kubectl.kubernetes.io/restartedAt\":\"$RESTART_AT\"}},\"spec\":{\"containers\":[{\"name\":\"$CONTAINER\",\"image\":\"$IMAGE\"}]}}}}" \
|
|
| jq '{name: .metadata.name, generation: .metadata.generation, image: .spec.template.spec.containers[0].image, paused: .spec.paused, restartedAt: .spec.template.metadata.annotations["kubectl.kubernetes.io/restartedAt"]}'
|
|
done
|
|
|
|
- name: verify-deploy
|
|
image: alpine
|
|
depends_on:
|
|
- Update deployment
|
|
commands:
|
|
- apk add --no-cache curl jq
|
|
- |
|
|
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
|
|
EXPECTED_IMAGE="viktorbarzin/realestatecrawler:${DRONE_BUILD_NUMBER}"
|
|
BASE_API="https://kubernetes:6443/api/v1/namespaces/realestate-crawler/pods"
|
|
DEPLOY_API="https://kubernetes:6443/apis/apps/v1/namespaces/realestate-crawler/deployments"
|
|
|
|
for DEPLOY in realestate-crawler-api realestate-crawler-celery realestate-crawler-celery-beat; do
|
|
echo "Verifying $DEPLOY..."
|
|
PODS_API="$BASE_API?labelSelector=app%3D$DEPLOY"
|
|
RS_API="https://kubernetes:6443/apis/apps/v1/namespaces/realestate-crawler/replicasets?labelSelector=app%3D$DEPLOY"
|
|
|
|
# Check deployment status (spec, conditions, paused, replicas)
|
|
DEPLOY_STATUS=$(curl -sfk "$DEPLOY_API/$DEPLOY" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Accept: application/json")
|
|
echo " Deployment spec image: $(echo "$DEPLOY_STATUS" | jq -r '.spec.template.spec.containers[0].image')"
|
|
echo " Deployment paused: $(echo "$DEPLOY_STATUS" | jq -r '.spec.paused // false')"
|
|
echo " Deployment status: replicas=$(echo "$DEPLOY_STATUS" | jq -r '.status.replicas // 0') updated=$(echo "$DEPLOY_STATUS" | jq -r '.status.updatedReplicas // 0') ready=$(echo "$DEPLOY_STATUS" | jq -r '.status.readyReplicas // 0') unavailable=$(echo "$DEPLOY_STATUS" | jq -r '.status.unavailableReplicas // 0')"
|
|
echo " Conditions:"
|
|
echo "$DEPLOY_STATUS" | jq -r '.status.conditions[]? | " \(.type): \(.status) (\(.reason // "unknown"))"' 2>/dev/null || echo " (none)"
|
|
|
|
# Check ReplicaSets
|
|
echo " ReplicaSets:"
|
|
curl -sfk "$RS_API" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Accept: application/json" | \
|
|
jq -r '.items[] | " \(.metadata.name) desired=\(.spec.replicas) ready=\(.status.readyReplicas // 0) image=\(.spec.template.spec.containers[0].image)"' 2>/dev/null || echo " (none)"
|
|
|
|
FOUND=0
|
|
for i in $(seq 1 60); do
|
|
RAW=$(curl -sfk "$PODS_API" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Accept: application/json")
|
|
|
|
# Debug: show all pod images and status periodically
|
|
if [ "$i" -eq 1 ] || [ "$i" -eq 10 ] || [ "$i" -eq 30 ]; then
|
|
echo " DEBUG (attempt $i): All pods for $DEPLOY:"
|
|
echo "$RAW" | jq -r '[.items[] | {
|
|
name: .metadata.name,
|
|
image: .spec.containers[0].image,
|
|
ready: ([.status.containerStatuses[]? | .ready] | first // "unknown"),
|
|
phase: .status.phase,
|
|
restarts: ([.status.containerStatuses[]? | .restartCount] | first // 0),
|
|
reason: ([.status.containerStatuses[]? | .state | to_entries[] | .value.reason // empty] | first // "running")
|
|
}] | .[] | " \(.name) image=\(.image) ready=\(.ready) phase=\(.phase) restarts=\(.restarts) reason=\(.reason)"' 2>/dev/null || echo " (no pods or parse error)"
|
|
fi
|
|
|
|
RESULT=$(echo "$RAW" | \
|
|
jq --arg img "$EXPECTED_IMAGE" '[.items[] | select(
|
|
(.status.containerStatuses[]? | .ready == true) and
|
|
(.spec.containers[]? | .image | endswith($img))
|
|
) | {name: .metadata.name, image: .spec.containers[0].image, started: .status.startTime}]')
|
|
|
|
COUNT=$(echo "$RESULT" | jq 'length' 2>/dev/null || echo 0)
|
|
echo " Attempt $i/60: $COUNT pod(s) ready with image matching $EXPECTED_IMAGE"
|
|
|
|
if [ "$COUNT" -gt 0 ] 2>/dev/null; then
|
|
echo "$RESULT" | jq -r '.[] | " \(.name) image=\(.image) started=\(.started)"'
|
|
echo "$DEPLOY is live!"
|
|
FOUND=1
|
|
break
|
|
fi
|
|
|
|
sleep 5
|
|
done
|
|
|
|
if [ "$FOUND" -ne 1 ]; then
|
|
echo " FINAL DEBUG: All pods for $DEPLOY:"
|
|
echo "$RAW" | jq -r '[.items[] | {
|
|
name: .metadata.name,
|
|
image: .spec.containers[0].image,
|
|
ready: ([.status.containerStatuses[]? | .ready] | first // "unknown"),
|
|
phase: .status.phase,
|
|
restarts: ([.status.containerStatuses[]? | .restartCount] | first // 0),
|
|
reason: ([.status.containerStatuses[]? | .state | to_entries[] | .value.reason // empty] | first // "running")
|
|
}] | .[] | " \(.name) image=\(.image) ready=\(.ready) phase=\(.phase) restarts=\(.restarts) reason=\(.reason)"' 2>/dev/null || echo " (no pods or parse error)"
|
|
echo "ERROR: No new ready pod for $DEPLOY with image $EXPECTED_IMAGE appeared within 5 minutes"
|
|
exit 1
|
|
fi
|
|
done
|