diff --git a/.github/workflows/harden-production.yml b/.github/workflows/configure-production.yml
similarity index 86%
rename from .github/workflows/harden-production.yml
rename to .github/workflows/configure-production.yml
index 29e0d59..b898b0b 100644
--- a/.github/workflows/harden-production.yml
+++ b/.github/workflows/configure-production.yml
@@ -1,11 +1,12 @@
---
-name: Harden production
+name: Configure production cluster
+run-name: Configure the production cluster for ${{ github.actor }}
"on":
workflow_dispatch:
inputs:
confirmation:
- description: Type APPLY_PRODUCTION_HARDENING
+ description: Type CONFIGURE_PRODUCTION
required: true
type: string
prove_scaling:
@@ -27,8 +28,8 @@ concurrency:
cancel-in-progress: false
jobs:
- metrics-server:
- name: Install and prove Metrics API
+ configure:
+ name: Configure production safeguards
runs-on: ubuntu-latest
environment: production
timeout-minutes: 20
@@ -39,11 +40,11 @@ jobs:
run: |
set -euo pipefail
[ "$GITHUB_REF" = refs/heads/main ] || {
- echo "Production hardening must run from main" >&2
+ echo "Production configuration must run from main" >&2
exit 1
}
- [ "$CONFIRMATION" = APPLY_PRODUCTION_HARDENING ] || {
- echo "confirmation must equal APPLY_PRODUCTION_HARDENING" >&2
+ [ "$CONFIRMATION" = CONFIGURE_PRODUCTION ] || {
+ echo "confirmation must equal CONFIGURE_PRODUCTION" >&2
exit 1
}
@@ -92,7 +93,7 @@ jobs:
API_ORIGIN: ${{ vars.API_ORIGIN }}
run: ./scripts/apply-network-policies.sh
- - name: Verify public application after hardening
+ - name: Verify the application after configuration
env:
API_ORIGIN: ${{ vars.API_ORIGIN }}
run: |
diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml
index 48540b1..e436607 100644
--- a/.github/workflows/deploy-production.yml
+++ b/.github/workflows/deploy-production.yml
@@ -1,5 +1,6 @@
---
-name: Deploy production
+name: Deploy to production
+run-name: Deploy ${{ inputs.version || 'a pinned image' }} to production
"on":
workflow_dispatch:
@@ -26,7 +27,7 @@ concurrency:
jobs:
deploy:
- name: Roll out immutable images
+ name: Deploy the selected server image
runs-on: ubuntu-latest
environment: production
timeout-minutes: 180
diff --git a/.github/workflows/ops-ci.yml b/.github/workflows/validate.yml
similarity index 92%
rename from .github/workflows/ops-ci.yml
rename to .github/workflows/validate.yml
index 5cdede1..f5b2558 100644
--- a/.github/workflows/ops-ci.yml
+++ b/.github/workflows/validate.yml
@@ -1,6 +1,7 @@
---
-# Ops CI — formate et valide l'IaC (prettier + terraform fmt).
-name: Ops CI
+# Validates the infrastructure definitions on every proposed change.
+name: Validate infrastructure
+run-name: Infrastructure checks for ${{ github.ref_name }}
"on":
push:
@@ -12,12 +13,12 @@ permissions:
contents: read
concurrency:
- group: ops-ci-${{ github.ref }}
+ group: infrastructure-validation-${{ github.ref }}
cancel-in-progress: true
jobs:
validate:
- name: Format & validate
+ name: Validate infrastructure definitions
runs-on: ubuntu-latest
steps:
- name: Checkout
diff --git a/Makefile b/Makefile
index e1db8d9..638182b 100644
--- a/Makefile
+++ b/Makefile
@@ -69,7 +69,7 @@ K8S_MANIFESTS := $(shell find k8s -name '*.yaml' ! -name '*.sops.yaml' | sort)
validate-templates secret-dry-run secret-apply secrets-dry-run secrets-apply \
backup-secret-configure backup-enable backup-run backup-verify backup-status \
cert-manager-install tls tls-staging tls-status \
- metrics hpa pdb load harden soft-affinity hard-affinity \
+ metrics hpa pdb load harden \
minikube minikube-host-preflight minikube-up minikube-deploy minikube-hosts minikube-smoke minikube-down
help: ## Affiche cette aide
@@ -443,12 +443,6 @@ hpa: metrics ## Applique le HPA du server (sauté tant que le Deployment n'exist
load: ## Génère de la charge HTTP (test autoscaling) contre l'hôte web
WEB_URL=http://$(WEB_HOST):30021 ./scripts/load.sh
-soft-affinity: ## Relâche l'anti-affinity required->preferred (clusters nodes < replicas)
- ./scripts/soft-affinity.sh on
-
-hard-affinity: ## Restaure l'anti-affinity stricte (preferred->required)
- ./scripts/soft-affinity.sh off
-
harden: pdb hpa ## Applique PDB + HPA (ce qui est prêt)
## --- Cluster local (minikube, sans DigitalOcean) ---------------------------
diff --git a/README.md b/README.md
index 98f9d8e..476923f 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-
+
diff --git a/docs/deployment-audit.md b/docs/deployment-audit.md
deleted file mode 100644
index 33f257a..0000000
--- a/docs/deployment-audit.md
+++ /dev/null
@@ -1,176 +0,0 @@
-# Production deployment audit
-
-Snapshot: 2026-07-30. This document records both verified production evidence
-and the remaining infrastructure gaps. It is not a cloud certification.
-
-## Architecture decision
-
-The nominal production topology is:
-
-- `opswarden.dev`: marketing site on Vercel (`opswarden-website`);
-- `app.opswarden.dev`: `client-web` on Vercel;
-- `api.opswarden.dev`: Rust HTTP API and WebSocket through Traefik on DOKS;
-- PostgreSQL, Redis and encrypted backup jobs on DOKS;
-- Terraform state and encrypted PostgreSQL backups in separate Spaces paths or
- buckets with separate restricted credentials.
-
-The Kubernetes `client-web` Deployment is retained only for local and optional
-self-hosted operation. Production CD deploys the Rust image only. Vercel must
-build from `client-web` with:
-
-```text
-OPSWARDEN_API_ORIGIN=https://api.opswarden.dev
-NEXT_PUBLIC_WS_URL=wss://api.opswarden.dev/ws
-```
-
-## Reproduced evidence
-
-- application release gate `30562337570` passes Rust formatting, strict Clippy,
- backend tests and coverage, web quality and tests, desktop packaging and the
- Playwright critical path;
-- release `v1.0.12` publishes the attested server image by immutable digest
- `sha256:6468bd3eb43b410ccc911c3dab0cade0696180932e765559df34bd88f2ead026`;
-- DOKS runs two ready server replicas with completed migration init containers,
- the dedicated runtime Secret and zero container restarts;
-- PostgreSQL owner, migrator, runtime and backup roles were reconciled by the
- production bootstrap Job after a validated custom-format database dump;
-- Let's Encrypt serves a certificate for `api.opswarden.dev`, and the public
- health, `/about.json` and WebSocket `101` checks pass through Traefik;
-- production CD run `30563381853` reproduced the immutable rollout and smoke
- checks from GitHub Actions;
-- observability CD run `30564398461` proves API HTTP 200, WebSocket 101,
- Prometheus target `up=1` and three loaded Alertmanager rules;
-- a production smoke using the official Alertmanager `v0.32.1` image pinned by
- digest delivered firing and resolved as two durable runs, with metrics
- `accepted +2` and `failed +0`; all temporary resources were removed;
-- bearer-token rotation and rollback reject each stale secret with HTTP 401 and
- accept the active secret with HTTP 202 while preserving connection identity.
-- hardening run `30565376997` installs two Metrics Server `v0.8.1` replicas
- pinned by multi-architecture digest and proves node, pod and HPA CPU metrics;
-- hardening run `30565632048` observes a controlled server HPA scale event from
- two to three ready replicas, then restores the nominal policy. Follow-up run
- `30565794256` proves `replicas=2`, `desired=2` and a healthy public API.
-- hardening run `30566269859` applies the complete application-namespace
- NetworkPolicy set in safe order. Public HTTP, `/about.json` and WebSocket
- remain available; the backup role reaches PostgreSQL while an unlabeled pod
- is denied.
-- deployment run `30566406249` rolls the immutable server from `v1.0.12` back
- to `v1.0.11`; run `30566497276` restores `v1.0.12`. Both pass rollout,
- application and Prometheus proofs. There is no SQL migration delta between
- the two releases.
-- Spaces Job `postgres-backup-manual-20260730191627` uploads an encrypted
- PostgreSQL dump and verifies all three remote files by downloading them;
- isolated Job `postgres-backup-verify-fv75q` validates SHA-256 and gzip,
- restores the schema and data, and proves the `users` relation plus successful
- SQLx migration records.
-- production Loki and Alloy rollouts are Ready; Prometheus reports both targets
- `up=1` and both availability rules healthy. A disposable, digest-pinned Pod
- produced `OPSWARDEN_LOG_PIPELINE_PROOF_20260730`, retrieved as one Loki stream
- and one entry, then removed.
-- the release-state helper was exercised against a disposable production
- ConfigMap: snapshot `before`, mutate to `after`, restore to `before`, then
- remove the proof resource. The test also caught and corrected stale
- Kubernetes `resourceVersion` metadata before the helper entered a release.
-- release `v1.0.13` passed quality, browser E2E, Linux, Windows and macOS
- packaging, immutable server/web image builds and provenance attestations in
- run `30580631905`. Production run `30582420348` resolved the server to
- `sha256:1acd2b9bff23bf61bd014cfe38d1680ab0f9a9bd301a15c8f167720d7b488919`,
- completed backup Job `postgres-backup-manual-20260730211238`, rolled two
- ready replicas and passed the application, Prometheus and backup checks.
-- independent post-deploy probes returned WebSocket `101` for the exact
- `https://app.opswarden.dev` Origin and for an originless service client, but
- `403` for a cross-site Origin. Prometheus reported both server replicas,
- kube-state-metrics, Loki and Alloy `up=1`, with every loaded rule healthy.
-- the guarded rollout exposed and permanently corrected three pre-server
- deployment defects: a forced namespace on mixed-namespace Alloy RBAC
- (PR #25), a kube-state-metrics liveness probe using the wrong port (PR #26),
- and reapplication of controller-owned immutable fields on the retained Loki
- PVC (PR #27). Each failed attempt restored its release snapshot before the
- application rollout.
-
-## Findings corrected on the audited branches
-
-1. The self-hosted web image no longer bakes `ws://localhost:8080/ws`; an empty
- setting uses the browser-origin fallback and Compose remains explicit.
-2. Namespaced manifests no longer force `default`; deployment commands select
- the reviewed namespace. SOPS streams remove legacy namespace metadata after
- decryption without modifying encrypted files.
-3. Production now deploys only Rust and uses distinct frontend and API origins.
-4. Traefik watches only the application namespace; TLS secret permissions are a
- namespace Role instead of cluster-wide Secret access.
-5. Terraform writes kubeconfig with `local_sensitive_file` mode `0600`, requires
- an explicit DOKS version, and exposes node size/count as reviewed inputs.
-6. GitHub Actions references used by the new workflows are pinned to commits.
-7. Database retry logs no longer include the raw SQLx error, which could contain
- connection details.
-8. Restore verification now requires the `users` relation and successful SQLx
- migration records, rather than accepting any connectable empty database.
-9. The standalone cAdvisor manifest is optional and excluded from the production
- deployment set; it is not treated as an observability stack.
-10. The canonical Ingress references `opswarden-api-tls`; the duplicate legacy
- Ingress was removed after the public certificate was verified.
-11. PostgreSQL now uses separate owner, migrator and DML-only runtime identities.
- The running server cannot execute migrations with its runtime credential.
-12. The guarded GitHub production environment now performs the same immutable
- rollout and strict public smoke test as the reviewed local procedure.
-13. Prometheus discovers the server endpoints through Kubernetes service
- discovery, Grafana provisions an Alertmanager dashboard, and alerts cover
- delivery failures, elevated rejection volume and duplicate ratio.
-14. The targeted server ingress policy explicitly preserves Traefik,
- client-web and Prometheus traffic. The first rollout exposed the danger of
- introducing an isolating policy incrementally; recovery PR #11 and run
- `30564398461` are the retained incident and recovery evidence.
-15. Backup and restore containers now share dump files through `fsGroup 20000`
- with group-readable permissions. DOKS' internal FRA1 Spaces endpoint is
- allowed as a single `/32` on TCP 443 for backup workloads only.
-16. Isolated restore omits per-database settings for cluster-global roles that
- `pg_dump --no-owner` does not export, while preserving and validating all
- application schema and data.
-17. Every application Docker `FROM` reference is digest-pinned and CI rejects
- regressions to mutable base-image tags.
-18. WebSocket handshakes enforce an exact browser Origin allowlist before
- upgrade. Originless native and service clients remain supported and still
- authenticate in-band.
-19. Mixed-namespace observability resources are applied without a forced
- namespace, and the retained Loki PVC is created only when absent. Bound
- controller fields are never treated as release-time changes.
-20. kube-state-metrics uses `/readyz` on its telemetry port and `/livez` on its
- main exposition port. The corrected production Pod remains Ready with zero
- restarts.
-
-## Remaining production hardening
-
-1. Encrypted upload, isolated restore and the five backup freshness/failure
- rules are proven. The first scheduled run, an observed 30-day retention
- deletion and an audited offline copy of the matching age identity remain
- time- or operator-gated follow-ups.
-2. The full application-namespace NetworkPolicy set is applied and its public,
- database allow and default-deny paths are proven. A future ACME renewal and
- each newly added external reaction must still be monitored against the
- explicit egress allowlist.
-3. The backup path omits role passwords intentionally. Disaster recovery
- therefore still requires the credential-rotation runbook after restoring
- schema and data.
-4. The deployment workflow now snapshots and restores its ConfigMaps,
- Deployments, NetworkPolicy, HPA and PDB as one release ID. Snapshot mutation
- and restoration are proven on a disposable production resource; a complete
- failed application rollout is intentionally not injected. Database
- migrations are forward-only expand/contract and remain outside automated
- rollback by design.
-5. Loki and Alloy are deployed with seven-day retained-volume storage, Grafana
- provisioning and healthy Prometheus availability alerts. A production log
- marker was collected and queried end to end. Multi-zone/object-store log
- durability remains out of scope.
-
-## Safe next gate
-
-The current production baseline is release `v1.0.13`, immutable server digest
-`sha256:1acd2b9bff23bf61bd014cfe38d1680ab0f9a9bd301a15c8f167720d7b488919`
-and successful guarded deployment `30582420348`.
-
-The API, WebSocket Origin policy and Alertmanager observability are deployed and
-reproducible through CD. Autoscaling, NetworkPolicies, immutable image rollback,
-guarded configuration restoration, centralized log ingestion and encrypted
-off-cluster backup with isolated restore are proven. Database rollback remains
-explicitly forward-only. Remaining gates are the time/operator-controlled items
-listed above, not unfinished P0-P3 implementation.
diff --git a/docs/recovery/current-contract.md b/docs/recovery/current-contract.md
deleted file mode 100644
index 5cc7dcf..0000000
--- a/docs/recovery/current-contract.md
+++ /dev/null
@@ -1 +0,0 @@
-# Backups au format actuel
diff --git a/docs/recovery/legacy-contract.md b/docs/recovery/legacy-contract.md
deleted file mode 100644
index 8f7b06a..0000000
--- a/docs/recovery/legacy-contract.md
+++ /dev/null
@@ -1 +0,0 @@
-# Historique prouvé avec ancien contrat
diff --git a/docs/runbooks/postgres-credential-rotation.md b/docs/runbooks/postgres-credential-rotation.md
index 5e68271..e1ca360 100644
--- a/docs/runbooks/postgres-credential-rotation.md
+++ b/docs/runbooks/postgres-credential-rotation.md
@@ -1,6 +1,6 @@
-# PostgreSQL credential rotation (step 2C)
+# Rotate PostgreSQL credentials
-OpsWarden uses four distinct PostgreSQL identities:
+OpsWarden uses five distinct PostgreSQL identities:
- `opswarden_admin`: bootstrap only, stored in `postgres-secret`;
- `opswarden_owner`: `NOLOGIN` owner of the database and schema;
diff --git a/docs/runbooks/spaces-backups.md b/docs/runbooks/spaces-backups.md
index 90fd2ff..ee99539 100644
--- a/docs/runbooks/spaces-backups.md
+++ b/docs/runbooks/spaces-backups.md
@@ -84,41 +84,18 @@ Un upload réussi ne suffit pas: le chantier n'est terminé qu'après le contrô
du checksum et la restauration réussie du dernier dump dans le PostgreSQL
isolé du Job de vérification.
-## Preuve de production du 30 juillet 2026
-
-Le contexte `do-fra1-opswarden-cluster`, namespace `default`, a produit les
-preuves suivantes:
-
-- `postgres-backup-manual-20260730191627`: dump réussi, trois fichiers chiffrés
- envoyés dans Spaces puis retéléchargés par `rclone check`, zéro différence;
-- `postgres-backup-verify-fv75q`: téléchargement du dernier backup, checksum
- SHA-256 et gzip valides, restauration complète dans PostgreSQL 18 isolé,
- relation `public.users` présente et migrations SQLx réussies;
-- CronJob `postgres-backup`: planification quotidienne à `02:17 UTC`, préfixe
- `production/postgres` et rétention `30d`.
-
-Dans DOKS FRA1, le nom régional Spaces se résout actuellement vers
-`10.114.15.254`. Les NetworkPolicies d'upload et de vérification autorisent
-uniquement cette adresse en `/32` sur TCP 443, tout en continuant à refuser les
-autres destinations RFC1918. Si DigitalOcean change cette résolution, validez
-la nouvelle adresse avant de modifier l'allowlist.
-
-## Supervision et preuves différées
+## Supervision
kube-state-metrics est limité aux `Jobs` et `CronJobs` du namespace `default`.
Prometheus alerte si le CronJob est absent ou suspendu, si un Job planifié
échoue, si aucune exécution planifiée n'a jamais réussi après 26 heures, ou si
le dernier succès dépasse 26 heures.
-Les preuves suivantes sont nécessairement différées:
-
-- première exécution réellement créée par le contrôleur CronJob à `02:17 UTC`;
-- suppression d'objets âgés de plus de `30d`.
-
-Ne marquez ces deux preuves terminées qu'après observation de l'état réel. La
-vérification complète par téléchargement reste quotidienne pour privilégier
+La vérification complète par téléchargement reste quotidienne pour privilégier
l'intégrité tant que la base est petite. Réévaluez ce choix si les frais de
-récupération Cold Storage ou la taille du dump deviennent significatifs.
+récupération ou la taille du dump deviennent significatifs. Vérifiez aussi
+périodiquement qu'une exécution est réellement créée à `02:17 UTC` et que les
+objets âgés de plus de `30d` sont supprimés.
Conservez une copie chiffrée ou matérielle de
`~/.config/sops/age/keys.txt` dans un emplacement de récupération distinct. Ne
diff --git a/k8s/server/server.hpa.yaml b/k8s/server/server.hpa.yaml
index 32cda0f..3a54909 100644
--- a/k8s/server/server.hpa.yaml
+++ b/k8s/server/server.hpa.yaml
@@ -1,7 +1,7 @@
# HorizontalPodAutoscaler du serveur OpsWarden (basé CPU).
# Requiert metrics-server (DOKS le fournit ; en local `make metrics`).
-# Scaler au-delà des replicas de base suppose une anti-affinity *preferred*
-# (pas required) sur le Deployment server (cf. scripts/soft-affinity.sh).
+# Scaler au-delà des replicas de base suppose l'anti-affinity souple définie
+# sur le Deployment server.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
diff --git a/scripts/apply-network-policies.sh b/scripts/apply-network-policies.sh
index db9fcd3..640bfe7 100755
--- a/scripts/apply-network-policies.sh
+++ b/scripts/apply-network-policies.sh
@@ -4,8 +4,7 @@ set -Eeuo pipefail
EXPECTED_CONTEXT=${EXPECTED_CONTEXT:-}
NAMESPACE=${NAMESPACE:-}
API_ORIGIN=${API_ORIGIN:-}
-backup_dir=${RUNNER_TEMP:-/tmp}/opswarden-network-policy-backup
-rollback_armed=1
+rollback_armed=0
for name in EXPECTED_CONTEXT NAMESPACE API_ORIGIN; do
if [ -z "${!name:-}" ]; then
@@ -19,6 +18,30 @@ if [ "$(kubectl config current-context)" != "$EXPECTED_CONTEXT" ]; then
exit 1
fi
+script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+# shellcheck source=scripts/release-state.sh
+source "$script_dir/release-state.sh"
+state_dir=$(mktemp -d "${RUNNER_TEMP:-/tmp}/opswarden-network-policy.XXXXXX")
+release_state_init "$EXPECTED_CONTEXT" "$state_dir"
+
+rollback() {
+ status=$?
+ trap - EXIT
+ if [ "$status" -ne 0 ] && [ "$rollback_armed" -eq 1 ]; then
+ echo ">> Échec du durcissement réseau; restauration des policies précédentes" >&2
+ if ! release_state_restore; then
+ echo ">> ATTENTION: la restauration d'au moins une NetworkPolicy a échoué" >&2
+ status=1
+ fi
+ fi
+ kubectl --context "$EXPECTED_CONTEXT" --namespace "$NAMESPACE" \
+ delete pod network-policy-allow-postgres network-policy-deny-postgres \
+ --ignore-not-found --wait=false >/dev/null 2>&1 || true
+ release_state_cleanup
+ exit "$status"
+}
+trap rollback EXIT
+
policies=(
"$NAMESPACE:allow-cluster-dns"
"$NAMESPACE:default-deny"
@@ -33,42 +56,13 @@ policies=(
"kube-public:traefik-public-ingress"
)
-mkdir -p "$backup_dir"
for policy in "${policies[@]}"; do
namespace=${policy%%:*}
name=${policy#*:}
- if kubectl --context "$EXPECTED_CONTEXT" --namespace "$namespace" \
- get networkpolicy "$name" -o yaml >"$backup_dir/$namespace--$name.yaml" 2>/dev/null; then
- :
- else
- : >"$backup_dir/$namespace--$name.absent"
- fi
+ release_state_snapshot "$namespace" networkpolicy "$name"
done
-rollback() {
- status=$?
- trap - EXIT
- if [ "$status" -ne 0 ] && [ "$rollback_armed" -eq 1 ]; then
- echo ">> Échec du durcissement réseau; restauration des policies précédentes" >&2
- for policy in "${policies[@]}"; do
- namespace=${policy%%:*}
- name=${policy#*:}
- if [ -f "$backup_dir/$namespace--$name.yaml" ]; then
- kubectl --context "$EXPECTED_CONTEXT" --namespace "$namespace" apply \
- -f "$backup_dir/$namespace--$name.yaml" >/dev/null || true
- else
- kubectl --context "$EXPECTED_CONTEXT" --namespace "$namespace" \
- delete networkpolicy "$name" --ignore-not-found >/dev/null || true
- fi
- done
- fi
- kubectl --context "$EXPECTED_CONTEXT" --namespace "$NAMESPACE" \
- delete pod network-policy-allow-postgres network-policy-deny-postgres \
- --ignore-not-found --wait=false >/dev/null 2>&1 || true
- exit "$status"
-}
-trap rollback EXIT
-
+rollback_armed=1
echo ">> Application ordonnée des autorisations réseau"
kubectl --context "$EXPECTED_CONTEXT" --namespace "$NAMESPACE" apply \
-f k8s/network-policies/allow-cluster-dns.yaml
diff --git a/scripts/deploy-production.sh b/scripts/deploy-production.sh
index 34b970c..7c63a46 100755
--- a/scripts/deploy-production.sh
+++ b/scripts/deploy-production.sh
@@ -39,7 +39,6 @@ release_state_snapshot observability configmap grafana-datasources
release_state_snapshot observability configmap grafana-dashboards-provisioning
release_state_snapshot observability configmap loki-config
release_state_snapshot observability configmap alloy-config
-release_state_snapshot observability persistentvolumeclaim loki-data
release_state_snapshot observability deployment loki
release_state_snapshot observability deployment alloy
release_state_snapshot observability deployment grafana
diff --git a/scripts/load.sh b/scripts/load.sh
index 0a41639..e550f62 100755
--- a/scripts/load.sh
+++ b/scripts/load.sh
@@ -7,10 +7,10 @@
# Usage :
# ./scripts/load.sh # 60s, auto-détecte hey/ab, sinon boucle curl
# DURATION=120 CONCURRENCY=100 ./scripts/load.sh
-# WEB_URL=http://api.opswarden.example:30021 ./scripts/load.sh
+# WEB_URL=http://app.opswarden.dev:30021 ./scripts/load.sh
set -euo pipefail
-WEB_URL="${WEB_URL:-http://app.opswarden.example:30021}"
+WEB_URL="${WEB_URL:-http://app.opswarden.dev:30021}"
DURATION="${DURATION:-60}"
CONCURRENCY="${CONCURRENCY:-50}"
diff --git a/scripts/soft-affinity.sh b/scripts/soft-affinity.sh
deleted file mode 100755
index 88caca3..0000000
--- a/scripts/soft-affinity.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env sh
-# Bascule l'anti-affinity des pods entre souple (preferred, défaut des manifests)
-# et dure (required) sur les déploiements répliqués — sans éditer les manifests.
-#
-# Le mode required garantit un replica par nœud mais exige de la capacité libre
-# pendant les rolling updates et le scale-up HPA. Le mode preferred conserve la
-# disponibilité si le cluster manque temporairement de nœuds.
-#
-# Usage :
-# ./soft-affinity.sh on # required -> preferred (petits clusters / local)
-# ./soft-affinity.sh off # preferred -> required (restaure la base)
-set -eu
-
-MODE="${1:-}"
-[ "$MODE" = "on" ] || [ "$MODE" = "off" ] || {
- echo "usage: $0 on|off" >&2; exit 2; }
-
-# app:namespace — déploiements répliqués avec anti-affinity.
-APP_NAMESPACE=${NAMESPACE:-default}
-TARGETS="server:${APP_NAMESPACE} client-web:${APP_NAMESPACE} traefik:kube-public"
-
-patch_for() {
- app="$1"
- if [ "$MODE" = "on" ]; then
- cat <