commit a3ce2d473c1e69f56b7833405e5d1beebb9a4d1c Author: Sergey Antropoff Date: Sat Jul 18 11:52:33 2026 +0300 Add self-contained API simulators lab with Compose and Helm. Ship Proxmox, oVirt, VMware, and OpenStack behind one Postgres stack, Makefile helpers (up/clean/helm up|down/push), and a Helm chart with namespace, Let's Encrypt ingresses, migrate inits, and skip-if-seeded jobs. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..edd64e2 --- /dev/null +++ b/Makefile @@ -0,0 +1,182 @@ +COMPOSE ?= docker compose +COMPOSE_INIT := $(COMPOSE) --profile init + +MIGRATE := proxmox-migrate ovirt-migrate vmware-migrate openstack-migrate +SEED := proxmox-seed ovirt-seed vmware-seed openstack-seed +APPS := postgres proxmox ovirt vmware openstack ovirt-gateway vmware-gateway openstack-gateway + +CHART ?= charts/api-simulators-lab +HELM_RELEASE ?= simulators +# Prefer values.yaml namespace; override with: make helm-up HELM_NAMESPACE=other +HELM_NAMESPACE ?= $(shell awk '/^namespace:/{print $$2; exit}' $(CHART)/values.yaml) + +# Second word for: make helm up | make helm down +ifeq (helm,$(firstword $(MAKECMDGOALS))) +HELM_CMD := $(word 2,$(MAKECMDGOALS)) +endif + +.PHONY: up down clean restart logs shell pull ps urls help init-clean \ + helm helm-up helm-down helm-template helm-lint helm-urls \ + helm-install helm-uninstall push + +help: ## Show targets + @echo "Compose:" + @awk 'BEGIN {FS = ":.*?## "} /^(up|down|clean|restart|logs|shell|pull|ps|urls|init-clean):.*?## / {printf " %-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @echo "" + @echo "Helm (make helm up | make helm down):" + @awk 'BEGIN {FS = ":.*?## "} /^helm(-[a-zA-Z0-9]+)?:.*?## / {printf " %-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @echo "" + @echo "Other:" + @awk 'BEGIN {FS = ":.*?## "} /^(help|push):.*?## / {printf " %-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +up: ## Compose: pull, migrate, start apps, seed +ifdef HELM_CMD + @: +else + $(COMPOSE) pull + $(COMPOSE) up -d postgres + $(COMPOSE) up -d --wait postgres + @echo ">> migrate" + @set -e; for s in $(MIGRATE); do \ + echo " $$s"; \ + $(COMPOSE_INIT) run --rm --no-deps $$s; \ + done + @echo ">> apps + gateways" + $(COMPOSE) up -d --wait $(APPS) + @echo ">> seed" + @set -e; for s in $(SEED); do \ + echo " $$s"; \ + $(COMPOSE_INIT) run --rm --no-deps $$s; \ + done + @$(MAKE) init-clean + @echo "Stack is up (migrate/seed containers removed)." + @$(MAKE) --no-print-directory urls +endif + +urls: ## Compose: print simulator URLs + @printf '\nEndpoints:\n' + @printf ' %-22s %s\n' 'Proxmox' 'http://localhost:8006/' + @printf ' %-22s %s\n' 'oVirt Engine' 'https://localhost:7443/' + @printf ' %-22s %s\n' 'oVirt Web UI' 'http://localhost:7500/' + @printf ' %-22s %s\n' 'VMware (HTTPS)' 'https://localhost:8443/' + @printf ' %-22s %s\n' 'VMware (HTTP)' 'http://localhost:8081/' + @printf ' %-22s %s\n' 'OpenStack Keystone' 'http://localhost:9500/' + @printf ' %-22s %s\n' 'OpenStack Nova' 'http://localhost:8774/' + @printf ' %-22s %s\n' 'OpenStack HTTPS' 'https://localhost:9443/' + @printf ' %-22s %s\n' 'PostgreSQL' '127.0.0.1:5432 (lab / lab)' + @printf '\n' + +init-clean: ## Compose: remove leftover migrate/seed containers + -$(COMPOSE_INIT) rm -f $(MIGRATE) $(SEED) 2>/dev/null + -@ids=$$(docker ps -aq --filter "label=com.docker.compose.project=api-simulators-lab" \ + --filter "name=migrate" --filter "name=seed" 2>/dev/null); \ + [ -z "$$ids" ] || docker rm -f $$ids 2>/dev/null || true + +down: ## Compose: stop and remove containers (keep DB volume) +ifdef HELM_CMD + @: +else + $(COMPOSE_INIT) down --remove-orphans +endif + +clean: ## Compose: stop stack and remove volumes / leftovers + $(COMPOSE_INIT) down -v --remove-orphans + @$(MAKE) init-clean + -@nets=$$(docker network ls -q --filter "label=com.docker.compose.project=api-simulators-lab" 2>/dev/null); \ + [ -z "$$nets" ] || docker network rm $$nets 2>/dev/null || true + @echo "Stack cleaned (containers, volumes, leftover init)." + +restart: ## Compose: restart app services + $(COMPOSE) restart $(filter-out postgres,$(APPS)) + +logs: ## Compose: follow logs (SERVICE=proxmox optional) + $(COMPOSE) logs -f --tail=200 $(SERVICE) + +shell: ## Compose: shell into a service (SERVICE=proxmox default) + $(COMPOSE) exec $(or $(SERVICE),proxmox) bash || \ + $(COMPOSE) exec $(or $(SERVICE),proxmox) sh + +pull: ## Compose: pull Hub images only + $(COMPOSE) pull + +ps: ## Compose: show container status + $(COMPOSE) ps -a + +# ── Helm ────────────────────────────────────────────────── + +helm: ## Dispatcher: make helm up | make helm down + @case "$(HELM_CMD)" in \ + up) $(MAKE) helm-up ;; \ + down) $(MAKE) helm-down ;; \ + "") echo "Usage: make helm up | make helm down"; exit 1 ;; \ + *) echo "Unknown: make helm $(HELM_CMD)"; echo "Usage: make helm up | make helm down"; exit 1 ;; \ + esac + +helm-up: ## Helm: deploy chart (warns + asks confirmation) + @printf '\n' + @printf 'WARNING: Configure Helm values before deploying to the cluster.\n' + @printf ' File: %s/values.yaml\n' '$(CHART)' + @printf ' Review at least:\n' + @printf ' - namespace, ingresses, certManager\n' + @printf ' - imageTag, ticketSigningKey, imagePullSecrets\n' + @printf ' - postgres.persistence (size, storageClass)\n' + @printf ' Let'\''s Encrypt needs public DNS to the Ingress (not *.lab.local).\n' + @printf ' Release: %s\n' '$(HELM_RELEASE)' + @printf ' Namespace: %s (from values.namespace)\n' '$(HELM_NAMESPACE)' + @printf '\n' +ifeq ($(HELM_YES),1) + @echo "HELM_YES=1 — skipping confirmation." +else + @printf 'Values are configured and it is OK to deploy? [y/N] ' + @read ans; \ + case "$$ans" in \ + y|Y|yes|YES) ;; \ + *) echo "Aborted."; exit 1 ;; \ + esac +endif + helm upgrade --install $(HELM_RELEASE) $(CHART) \ + --namespace $(HELM_NAMESPACE) --create-namespace --wait \ + --set namespace=$(HELM_NAMESPACE) + @$(MAKE) --no-print-directory helm-urls + +helm-down: ## Helm: uninstall release + helm uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) || true + @echo "Helm release $(HELM_RELEASE) removed from namespace $(HELM_NAMESPACE)." + +helm-install: helm-up ## Alias of helm-up + +helm-uninstall: helm-down ## Alias of helm-down + +helm-template: ## Helm: render manifests + helm template $(HELM_RELEASE) $(CHART) --namespace $(HELM_NAMESPACE) + +helm-lint: ## Helm: lint chart + helm lint $(CHART) + +helm-urls: ## Helm: print Ingress hosts from values + @printf '\nIngress hosts (TLS / Let'\''s Encrypt):\n' + @awk '\ + /^ingresses:/{p=1; next} \ + p && /^[^ #\t]/{exit} \ + p && /^ [a-zA-Z0-9_-]+:/{name=$$1; sub(/:/,"",name)} \ + p && /^ host:/{host=$$2; printf " %-16s https://%s/\n", name, host} \ + ' $(CHART)/values.yaml + @printf '\n' + +push: ## git add ., multiline commit (Ctrl-D), push origin + @set -e; \ + git add .; \ + echo "=== staged ==="; \ + git status --short; \ + echo; \ + if git diff --cached --quiet; then \ + echo "Nothing to commit — pushing current branch."; \ + else \ + echo "Enter commit message, then Ctrl-D:"; \ + msg=$$(cat &2; exit 1; fi; \ + git commit -m "$$msg"; \ + fi; \ + echo "Pushing to both remotes:"; \ + git remote get-url --push --all origin | sed 's/^/ - /'; \ + git push origin HEAD diff --git a/README.md b/README.md new file mode 100644 index 0000000..14bd8ea --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +**Language / Язык:** [English](README.md) | [Русский](README.ru.md) + +# API simulators lab (self-contained) + +Four published simulators and one PostgreSQL. No source checkouts required. +Run locally with Docker Compose, or on Kubernetes with the Helm chart. + +## Layout + +``` +simulators/ + Makefile + docker-compose.yml + docker/ + postgres-init/01-simulators.sql + tls/server.{crt,key} # shared lab TLS for Compose gateways + ovirt/ovirt-engine.conf + vmware/vmware-ports.conf + openstack/openstack-ports.conf + charts/api-simulators-lab/ # Helm: all four simulators + Ingress +``` + +Images: `inecs/proxmox-api-simulator`, `inecs/ovirt-api-simulator`, +`inecs/vmware-api-simulator`, `inecs/openstack-api-simulator`. + +--- + +## Docker Compose + +Postgres data lives in the Compose volume `postgres-data`. + +```bash +cd /Users/inecs/Разработка/simulators +make help +make up # pull → migrate → apps → seed; prints URLs +make urls # print Compose endpoints again +make logs # or: make logs SERVICE=proxmox +make restart +make down # stop containers, keep DB volume +make clean # stop + remove volumes, leftovers, project networks +make shell # or: make shell SERVICE=ovirt +make push # git add . → multiline commit (Ctrl-D) → push origin +``` + +`make up` runs migrate/seed via `docker compose run --rm` (profile `init`), so those +containers do not stay in `Exited` state. + +Equivalent Compose: + +```bash +docker compose pull +docker compose up -d +``` + +The first start creates databases via `docker/postgres-init` (only on an empty +`postgres-data` volume). Seed jobs run once after each simulator becomes healthy. + +Pin an image tag: + +```bash +IMAGE_TAG=0.1.0 docker compose up -d +# or: IMAGE_TAG=0.1.0 make up +``` + +### Compose endpoints + +| Simulator | URL | +|---|---| +| Proxmox | http://localhost:8006/ | +| oVirt Engine | https://localhost:7443/ | +| oVirt Web UI | http://localhost:7500/ | +| VMware (HTTPS) | https://localhost:8443/ | +| VMware (HTTP) | http://localhost:8081/ | +| OpenStack Keystone | http://localhost:9500/ | +| OpenStack Nova | http://localhost:8774/ | +| OpenStack HTTPS | https://localhost:9443/ | +| PostgreSQL | `127.0.0.1:5432` (user `lab` / `lab`) | + +### Reset the Compose database + +```bash +make clean +make up +# or: docker compose down -v && docker compose up -d +``` + +--- + +## Kubernetes (Helm) + +Chart: `charts/api-simulators-lab`. + +Configure before deploy: `charts/api-simulators-lab/values.yaml` + +| Key | Purpose | +|---|---| +| `namespace` | Target namespace (`simulators` by default; `namespaceCreate: true`) | +| `ingresses` | Hosts, TLS secrets, annotations (one Ingress per entry) | +| `certManager` | Let's Encrypt via cert-manager (`letsencrypt-prod`) | +| `imageTag` / secrets / `postgres.persistence` | Images, keys, PVC | + +Default Ingress hosts (override for real DNS before Let's Encrypt): + +| Simulator | Host | +|---|---| +| Proxmox | https://proxmox.lab.local/ | +| oVirt | https://ovirt.lab.local/ | +| VMware | https://vmware.lab.local/ | +| OpenStack | https://openstack.lab.local/ | + +```bash +make helm up # warns → confirm [y/N] → helm upgrade --install +make helm down # uninstall release +make helm-urls # print Ingress hosts from values +make helm-template +make helm-lint +``` + +Skip the confirmation prompt (CI): `make helm-up HELM_YES=1` + +Override namespace without editing values: `HELM_NAMESPACE=other make helm-up` + +Equivalent Helm: + +```bash +helm upgrade --install simulators charts/api-simulators-lab \ + --namespace simulators --create-namespace --wait +``` + +### How the chart starts + +1. **Namespace** — created from `values.namespace` when `namespaceCreate: true`. +2. **PostgreSQL** — StatefulSet + init SQL for four simulator databases. +3. **Simulators** — Deployments with init containers: + - `wait-postgres` → `migrate` (idempotent; no-op if schema is current). +4. **Seed** — post-install / post-upgrade Jobs per simulator. + - Each Job waits until the app is ready, then seeds. + - `seed.skipIfSql` skips when the DB already has lab data (no wipe/re-seed). +5. **Ingress** — TLS via cert-manager annotation `cert-manager.io/cluster-issuer`. + - Set `certManager.createClusterIssuer: true` and `certManager.email` to create + ClusterIssuers from the chart; otherwise an existing `letsencrypt-prod` is used. + - Let's Encrypt HTTP-01 needs **public DNS** pointing at the Ingress controller + (not `*.lab.local`). + +--- + +## Git + +`origin` pushes to both remotes (same pattern as other simulators): + +- `git@github.com:sergeyantropoff/simulators.git` +- `ssh://git@git.antropoff.ru:30022/DevOpsTools/Simulators.git` + +```bash +make push # stage all → multiline commit message (end with Ctrl-D) → push origin +``` + +Same UX as `proxmox_api_simulator`. + +--- + +Laboratory defaults only — do not expose this stack to the public Internet without rotating secrets (`ticketSigningKey`, DB passwords, Ingress hosts). diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..9785d59 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,163 @@ +**Language / Язык:** [English](README.md) | [Русский](README.ru.md) + +# Лаборатория API-симуляторов (автономная) + +Четыре опубликованных симулятора и один PostgreSQL. Исходники репозиториев не нужны. +Локально — Docker Compose; в Kubernetes — Helm-чарт. + +## Структура + +``` +simulators/ + Makefile + docker-compose.yml + docker/ + postgres-init/01-simulators.sql + tls/server.{crt,key} # общие lab TLS для Compose gateway + ovirt/ovirt-engine.conf + vmware/vmware-ports.conf + openstack/openstack-ports.conf + charts/api-simulators-lab/ # Helm: все четыре симулятора + Ingress +``` + +Образы: `inecs/proxmox-api-simulator`, `inecs/ovirt-api-simulator`, +`inecs/vmware-api-simulator`, `inecs/openstack-api-simulator`. + +--- + +## Docker Compose + +Данные Postgres — в Compose volume `postgres-data`. + +```bash +cd /Users/inecs/Разработка/simulators +make help +make up # pull → migrate → apps → seed; печатает URL +make urls # снова вывести Compose-эндпоинты +make logs # или: make logs SERVICE=proxmox +make restart +make down # остановить контейнеры, volume БД сохранить +make clean # остановить + удалить volumes, мусор и сети проекта +make shell # или: make shell SERVICE=ovirt +make push # git add . → многострочный commit (Ctrl-D) → push origin +``` + +`make up` гоняет migrate/seed через `docker compose run --rm` (profile `init`), +поэтому эти контейнеры не остаются в статусе `Exited`. + +Эквивалент через Compose: + +```bash +docker compose pull +docker compose up -d +``` + +При первом старте БД создаются из `docker/postgres-init` (только на пустом +volume `postgres-data`). Seed выполняется один раз после готовности каждого симулятора. + +Закрепить тег образа: + +```bash +IMAGE_TAG=0.1.0 docker compose up -d +# или: IMAGE_TAG=0.1.0 make up +``` + +### Compose-эндпоинты + +| Симулятор | URL | +|---|---| +| Proxmox | http://localhost:8006/ | +| oVirt Engine | https://localhost:7443/ | +| oVirt Web UI | http://localhost:7500/ | +| VMware (HTTPS) | https://localhost:8443/ | +| VMware (HTTP) | http://localhost:8081/ | +| OpenStack Keystone | http://localhost:9500/ | +| OpenStack Nova | http://localhost:8774/ | +| OpenStack HTTPS | https://localhost:9443/ | +| PostgreSQL | `127.0.0.1:5432` (пользователь `lab` / `lab`) | + +### Сброс БД Compose + +```bash +make clean +make up +# или: docker compose down -v && docker compose up -d +``` + +--- + +## Kubernetes (Helm) + +Чарт: `charts/api-simulators-lab`. + +Перед деплоем настройте: `charts/api-simulators-lab/values.yaml` + +| Ключ | Назначение | +|---|---| +| `namespace` | Целевой namespace (`simulators` по умолчанию; `namespaceCreate: true`) | +| `ingresses` | Хосты, TLS secrets, annotations (по одному Ingress на запись) | +| `certManager` | Let's Encrypt через cert-manager (`letsencrypt-prod`) | +| `imageTag` / секреты / `postgres.persistence` | Образы, ключи, PVC | + +Хосты Ingress по умолчанию (для Let's Encrypt замените на публичный DNS): + +| Симулятор | Host | +|---|---| +| Proxmox | https://proxmox.lab.local/ | +| oVirt | https://ovirt.lab.local/ | +| VMware | https://vmware.lab.local/ | +| OpenStack | https://openstack.lab.local/ | + +```bash +make helm up # предупреждение → подтверждение [y/N] → helm upgrade --install +make helm down # uninstall релиза +make helm-urls # хосты Ingress из values +make helm-template +make helm-lint +``` + +Без подтверждения (CI): `make helm-up HELM_YES=1` + +Сменить namespace без правки файла: `HELM_NAMESPACE=other make helm-up` + +Эквивалент Helm: + +```bash +helm upgrade --install simulators charts/api-simulators-lab \ + --namespace simulators --create-namespace --wait +``` + +### Как стартует чарт + +1. **Namespace** — создаётся из `values.namespace`, если `namespaceCreate: true`. +2. **PostgreSQL** — StatefulSet + init SQL на четыре БД симуляторов. +3. **Симуляторы** — Deployments с init-контейнерами: + - `wait-postgres` → `migrate` (идемпотентно; ничего не делает, если схема уже актуальна). +4. **Seed** — Jobs post-install / post-upgrade на каждый симулятор. + - Job ждёт ready приложения, затем сидирует. + - `seed.skipIfSql` пропускает seed, если в БД уже есть лабораторные данные. +5. **Ingress** — TLS через аннотацию cert-manager `cert-manager.io/cluster-issuer`. + - `certManager.createClusterIssuer: true` и `certManager.email` — создать + ClusterIssuer из чарта; иначе ожидается уже существующий `letsencrypt-prod`. + - Let's Encrypt HTTP-01 требует **публичный DNS** на Ingress-контроллер + (не `*.lab.local`). + +--- + +## Git + +`origin` пушит в оба remote (как у остальных симуляторов): + +- `git@github.com:sergeyantropoff/simulators.git` +- `ssh://git@git.antropoff.ru:30022/DevOpsTools/Simulators.git` + +```bash +make push # stage all → многострочное сообщение (конец — Ctrl-D) → push origin +``` + +Тот же UX, что в `proxmox_api_simulator`. + +--- + +Только лабораторные defaults — не выставляйте стек в публичный Интернет без смены +секретов (`ticketSigningKey`, пароли БД, хосты Ingress). diff --git a/charts/api-simulators-lab/.helmignore b/charts/api-simulators-lab/.helmignore new file mode 100644 index 0000000..1fab054 --- /dev/null +++ b/charts/api-simulators-lab/.helmignore @@ -0,0 +1,3 @@ +.DS_Store +*.tgz +charts/ diff --git a/charts/api-simulators-lab/Chart.yaml b/charts/api-simulators-lab/Chart.yaml new file mode 100644 index 0000000..f073ddb --- /dev/null +++ b/charts/api-simulators-lab/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: api-simulators-lab +description: Laboratory stack — Proxmox, oVirt, VMware and OpenStack API simulators +type: application +version: 0.1.0 +appVersion: "latest" +keywords: + - simulators + - proxmox + - ovirt + - vmware + - openstack +maintainers: + - name: inecs diff --git a/charts/api-simulators-lab/templates/NOTES.txt b/charts/api-simulators-lab/templates/NOTES.txt new file mode 100644 index 0000000..dd89c73 --- /dev/null +++ b/charts/api-simulators-lab/templates/NOTES.txt @@ -0,0 +1,28 @@ +1. Namespace: {{ include "lab.namespace" . }} + +2. Point DNS (or /etc/hosts for local-only) at the Ingress controller: +{{- range $name, $ing := .Values.ingresses }} +{{- if $ing.enabled }} + {{ $ing.host }} +{{- end }} +{{- end }} + +3. Open simulators (TLS via cert-manager / Let's Encrypt when enabled): +{{- range $name, $ing := .Values.ingresses }} +{{- if $ing.enabled }} +{{- $https := or $ing.tls $.Values.certManager.enabled }} + http{{ if $https }}s{{ end }}://{{ $ing.host }}{{ $ing.path | default "/" }} +{{- end }} +{{- end }} + +{{- if .Values.certManager.enabled }} + +cert-manager ClusterIssuer: {{ include "lab.clusterIssuer" . }} +{{- if .Values.certManager.createClusterIssuer }} + (created by this chart; email={{ .Values.certManager.email }}) +{{- else }} + (expected to already exist in the cluster) +{{- end }} +{{- end }} + +Configure namespace / hosts / TLS under `.Values.namespace`, `.Values.ingresses`, `.Values.certManager`. diff --git a/charts/api-simulators-lab/templates/_helpers.tpl b/charts/api-simulators-lab/templates/_helpers.tpl new file mode 100644 index 0000000..a81cd5e --- /dev/null +++ b/charts/api-simulators-lab/templates/_helpers.tpl @@ -0,0 +1,69 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "lab.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "lab.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "lab.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "lab.labels" -}} +helm.sh/chart: {{ include "lab.chart" . }} +{{ include "lab.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "lab.selectorLabels" -}} +app.kubernetes.io/name: {{ include "lab.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "lab.postgresHost" -}} +{{- printf "%s-postgres" (include "lab.fullname" .) }} +{{- end }} + +{{- define "lab.namespace" -}} +{{- .Values.namespace | default .Release.Namespace }} +{{- end }} + +{{- define "lab.databaseUrl" -}} +{{- $sim := .sim -}} +{{- printf "postgresql://%s:%s@%s:5432/%s" $sim.db.user $sim.db.password (include "lab.postgresHost" .root) $sim.db.name }} +{{- end }} + +{{- define "lab.commonEnv" -}} +- name: TICKET_SIGNING_KEY + valueFrom: + secretKeyRef: + name: {{ include "lab.fullname" . }} + key: ticketSigningKey +- name: LOG_LEVEL + value: {{ .Values.logLevel | quote }} +{{- end }} + +{{- define "lab.clusterIssuer" -}} +{{- if .Values.certManager.useStaging }} +{{- .Values.certManager.stagingIssuerName }} +{{- else }} +{{- .Values.certManager.issuerName }} +{{- end }} +{{- end }} diff --git a/charts/api-simulators-lab/templates/clusterissuer.yaml b/charts/api-simulators-lab/templates/clusterissuer.yaml new file mode 100644 index 0000000..0be0932 --- /dev/null +++ b/charts/api-simulators-lab/templates/clusterissuer.yaml @@ -0,0 +1,42 @@ +{{- if and .Values.certManager.enabled .Values.certManager.createClusterIssuer }} +{{- $solverClass := .Values.certManager.solverIngressClassName | default "nginx" }} +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: {{ .Values.certManager.issuerName }} + labels: + {{- include "lab.labels" . | nindent 4 }} +spec: + acme: + email: {{ required "certManager.email is required when createClusterIssuer=true" .Values.certManager.email | quote }} + server: {{ .Values.certManager.server | quote }} + privateKeySecretRef: + name: {{ printf "%s-account-key" .Values.certManager.issuerName }} + solvers: + - http01: + ingress: + {{- if $solverClass }} + ingressClassName: {{ $solverClass }} + {{- end }} +--- +{{- if .Values.certManager.createStagingIssuer }} +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: {{ .Values.certManager.stagingIssuerName }} + labels: + {{- include "lab.labels" . | nindent 4 }} +spec: + acme: + email: {{ .Values.certManager.email | quote }} + server: {{ .Values.certManager.stagingServer | quote }} + privateKeySecretRef: + name: {{ printf "%s-account-key" .Values.certManager.stagingIssuerName }} + solvers: + - http01: + ingress: + {{- if $solverClass }} + ingressClassName: {{ $solverClass }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/api-simulators-lab/templates/ingress.yaml b/charts/api-simulators-lab/templates/ingress.yaml new file mode 100644 index 0000000..6648693 --- /dev/null +++ b/charts/api-simulators-lab/templates/ingress.yaml @@ -0,0 +1,52 @@ +{{- range $name, $ing := .Values.ingresses }} +{{- if $ing.enabled }} +{{- $svcName := printf "%s-%s" (include "lab.fullname" $) $ing.service }} +{{- $className := $ing.className | default $.Values.ingressClassName }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "lab.fullname" $ }}-{{ $name }} + namespace: {{ include "lab.namespace" $ }} + labels: + {{- include "lab.labels" $ | nindent 4 }} + app.kubernetes.io/component: ingress-{{ $name }} + annotations: + {{- if $.Values.certManager.enabled }} + cert-manager.io/cluster-issuer: {{ include "lab.clusterIssuer" $ | quote }} + {{- end }} + {{- with $ing.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if $className }} + ingressClassName: {{ $className | quote }} + {{- end }} + {{- if $ing.tls }} + tls: + {{- range $ing.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- else if $.Values.certManager.enabled }} + tls: + - hosts: + - {{ $ing.host | quote }} + secretName: {{ printf "%s-%s-tls" (include "lab.fullname" $) $name }} + {{- end }} + rules: + - host: {{ $ing.host | quote }} + http: + paths: + - path: {{ $ing.path | default "/" | quote }} + pathType: {{ $ing.pathType | default "Prefix" }} + backend: + service: + name: {{ $svcName }} + port: + number: {{ $ing.port }} +--- +{{- end }} +{{- end }} diff --git a/charts/api-simulators-lab/templates/namespace.yaml b/charts/api-simulators-lab/templates/namespace.yaml new file mode 100644 index 0000000..f8442e0 --- /dev/null +++ b/charts/api-simulators-lab/templates/namespace.yaml @@ -0,0 +1,8 @@ +{{- if .Values.namespaceCreate }} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ include "lab.namespace" . }} + labels: + {{- include "lab.labels" . | nindent 4 }} +{{- end }} diff --git a/charts/api-simulators-lab/templates/postgres-configmap.yaml b/charts/api-simulators-lab/templates/postgres-configmap.yaml new file mode 100644 index 0000000..8f5f504 --- /dev/null +++ b/charts/api-simulators-lab/templates/postgres-configmap.yaml @@ -0,0 +1,17 @@ +{{- if .Values.postgres.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "lab.fullname" . }}-postgres-init + namespace: {{ include "lab.namespace" . }} + labels: + {{- include "lab.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres +data: + 01-simulators.sql: | +{{- range .Values.postgres.initDatabases }} + CREATE USER {{ .user }} WITH PASSWORD '{{ .password }}'; + CREATE DATABASE {{ .database }} OWNER {{ .user }}; + +{{- end }} +{{- end }} diff --git a/charts/api-simulators-lab/templates/postgres.yaml b/charts/api-simulators-lab/templates/postgres.yaml new file mode 100644 index 0000000..fd83817 --- /dev/null +++ b/charts/api-simulators-lab/templates/postgres.yaml @@ -0,0 +1,110 @@ +{{- if .Values.postgres.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "lab.postgresHost" . }} + namespace: {{ include "lab.namespace" . }} + labels: + {{- include "lab.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres +spec: + type: ClusterIP + ports: + - name: postgres + port: 5432 + targetPort: postgres + protocol: TCP + selector: + {{- include "lab.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "lab.postgresHost" . }} + namespace: {{ include "lab.namespace" . }} + labels: + {{- include "lab.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres +spec: + serviceName: {{ include "lab.postgresHost" . }} + replicas: 1 + selector: + matchLabels: + {{- include "lab.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: postgres + template: + metadata: + labels: + {{- include "lab.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: postgres + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: postgres + image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}" + imagePullPolicy: {{ .Values.postgres.image.pullPolicy }} + ports: + - name: postgres + containerPort: 5432 + protocol: TCP + env: + - name: POSTGRES_USER + value: {{ .Values.postgres.auth.username | quote }} + - name: POSTGRES_DB + value: {{ .Values.postgres.auth.database | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "lab.fullname" . }} + key: postgres-password + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + - name: init + mountPath: /docker-entrypoint-initdb.d + readOnly: true + readinessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgres.auth.username | quote }}, "-d", {{ .Values.postgres.auth.database | quote }}] + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 12 + livenessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgres.auth.username | quote }}, "-d", {{ .Values.postgres.auth.database | quote }}] + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 3 + {{- with .Values.postgres.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: init + configMap: + name: {{ include "lab.fullname" . }}-postgres-init + {{- if not .Values.postgres.persistence.enabled }} + - name: data + emptyDir: {} + {{- end }} + {{- if .Values.postgres.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + {{- if .Values.postgres.persistence.storageClass }} + storageClassName: {{ .Values.postgres.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.postgres.persistence.size }} + {{- end }} +{{- end }} diff --git a/charts/api-simulators-lab/templates/secret.yaml b/charts/api-simulators-lab/templates/secret.yaml new file mode 100644 index 0000000..75b0c07 --- /dev/null +++ b/charts/api-simulators-lab/templates/secret.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "lab.fullname" . }} + namespace: {{ include "lab.namespace" . }} + labels: + {{- include "lab.labels" . | nindent 4 }} +type: Opaque +stringData: + ticketSigningKey: {{ .Values.ticketSigningKey | quote }} + postgres-password: {{ .Values.postgres.auth.password | quote }} +{{- range $name, $sim := .Values.simulators }} +{{- if and $sim.enabled $sim.db }} + {{ $name }}-db-password: {{ $sim.db.password | quote }} +{{- end }} +{{- end }} diff --git a/charts/api-simulators-lab/templates/seed-jobs.yaml b/charts/api-simulators-lab/templates/seed-jobs.yaml new file mode 100644 index 0000000..327f149 --- /dev/null +++ b/charts/api-simulators-lab/templates/seed-jobs.yaml @@ -0,0 +1,92 @@ +{{- if .Values.seedJobs.enabled }} +{{- range $name, $sim := .Values.simulators }} +{{- if and $sim.enabled $sim.seed $sim.seed.enabled }} +{{- $fullname := printf "%s-%s" (include "lab.fullname" $) $name }} +{{- $tag := $sim.image.tag | default $.Values.imageTag }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ $fullname }}-seed + namespace: {{ include "lab.namespace" $ }} + labels: + {{- include "lab.labels" $ | nindent 4 }} + app.kubernetes.io/component: {{ $name }}-seed + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "10" + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded +spec: + backoffLimit: {{ $.Values.seedJobs.backoffLimit }} + ttlSecondsAfterFinished: {{ $.Values.seedJobs.ttlSecondsAfterFinished }} + template: + metadata: + labels: + {{- include "lab.selectorLabels" $ | nindent 8 }} + app.kubernetes.io/component: {{ $name }}-seed + spec: + restartPolicy: OnFailure + {{- with $.Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + - name: wait-ready + image: curlimages/curl:8.12.1 + imagePullPolicy: IfNotPresent + command: + - sh + - -ec + - | + url="http://{{ $fullname }}:{{ $sim.port }}{{ $sim.healthPath }}" + echo "waiting for $url" + until curl -fsS "$url" >/dev/null; do sleep 3; done + containers: + - name: seed + image: "{{ $sim.image.repository }}:{{ $tag }}" + imagePullPolicy: {{ $sim.image.pullPolicy }} + # Skip when skipIfSql returns a row (DB already seeded / has lab data). + command: + - python + - -c + - | + import asyncio, os, subprocess, sys + import asyncpg + + skip_sql = os.environ.get("SEED_SKIP_IF_SQL", "").strip() + seed_cmd = {{ $sim.seed.command | toJson }} + + async def already_seeded() -> bool: + if not skip_sql: + return False + conn = await asyncpg.connect(os.environ["DATABASE_URL"]) + try: + return await conn.fetchval(skip_sql) is not None + except Exception as exc: # noqa: BLE001 — empty/unmigrated DB + print(f"seed: skip-check deferred ({exc})") + return False + finally: + await conn.close() + + if asyncio.run(already_seeded()): + print("seed: database already has data — skipping") + raise SystemExit(0) + print("seed: applying", seed_cmd) + raise SystemExit(subprocess.call(seed_cmd)) + env: + {{- include "lab.commonEnv" $ | nindent 12 }} + - name: DATABASE_URL + value: {{ include "lab.databaseUrl" (dict "root" $ "sim" $sim) | quote }} + - name: SEED_SKIP_IF_SQL + value: {{ ($sim.seed.skipIfSql | default "") | quote }} + {{- range $k, $v := $sim.env }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- range $k, $v := ($sim.seed.env | default dict) }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} +--- +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/api-simulators-lab/templates/simulators.yaml b/charts/api-simulators-lab/templates/simulators.yaml new file mode 100644 index 0000000..d9aa50d --- /dev/null +++ b/charts/api-simulators-lab/templates/simulators.yaml @@ -0,0 +1,111 @@ +{{- range $name, $sim := .Values.simulators }} +{{- if $sim.enabled }} +{{- $fullname := printf "%s-%s" (include "lab.fullname" $) $name }} +{{- $tag := $sim.image.tag | default $.Values.imageTag }} +apiVersion: v1 +kind: Service +metadata: + name: {{ $fullname }} + namespace: {{ include "lab.namespace" $ }} + labels: + {{- include "lab.labels" $ | nindent 4 }} + app.kubernetes.io/component: {{ $name }} +spec: + type: ClusterIP + ports: + - name: http + port: {{ $sim.port }} + targetPort: http + protocol: TCP + selector: + {{- include "lab.selectorLabels" $ | nindent 4 }} + app.kubernetes.io/component: {{ $name }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $fullname }} + namespace: {{ include "lab.namespace" $ }} + labels: + {{- include "lab.labels" $ | nindent 4 }} + app.kubernetes.io/component: {{ $name }} +spec: + replicas: {{ $sim.replicas | default 1 }} + selector: + matchLabels: + {{- include "lab.selectorLabels" $ | nindent 6 }} + app.kubernetes.io/component: {{ $name }} + template: + metadata: + labels: + {{- include "lab.selectorLabels" $ | nindent 8 }} + app.kubernetes.io/component: {{ $name }} + spec: + {{- with $.Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + - name: wait-postgres + image: "{{ $.Values.postgres.image.repository }}:{{ $.Values.postgres.image.tag }}" + imagePullPolicy: {{ $.Values.postgres.image.pullPolicy }} + command: + - sh + - -ec + - | + until pg_isready -h {{ include "lab.postgresHost" $ }} -p 5432 -U {{ $.Values.postgres.auth.username }}; do + echo "waiting for postgres..."; sleep 2; + done + {{- if and $sim.migrate $sim.migrate.enabled }} + - name: migrate + image: "{{ $sim.image.repository }}:{{ $tag }}" + imagePullPolicy: {{ $sim.image.pullPolicy }} + # Idempotent: migrate_cli applies 0 migrations when schema is already current. + command: {{- toYaml $sim.migrate.command | nindent 12 }} + env: + {{- include "lab.commonEnv" $ | nindent 12 }} + - name: DATABASE_URL + value: {{ include "lab.databaseUrl" (dict "root" $ "sim" $sim) | quote }} + {{- range $k, $v := $sim.env }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- end }} + containers: + - name: simulator + image: "{{ $sim.image.repository }}:{{ $tag }}" + imagePullPolicy: {{ $sim.image.pullPolicy }} + ports: + - name: http + containerPort: {{ $sim.port }} + protocol: TCP + env: + {{- include "lab.commonEnv" $ | nindent 12 }} + - name: DATABASE_URL + value: {{ include "lab.databaseUrl" (dict "root" $ "sim" $sim) | quote }} + {{- range $k, $v := $sim.env }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + readinessProbe: + httpGet: + path: {{ $sim.healthPath }} + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 10 + livenessProbe: + httpGet: + path: {{ $sim.healthPath }} + port: http + initialDelaySeconds: 30 + periodSeconds: 20 + timeoutSeconds: 3 + {{- with $sim.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} +--- +{{- end }} +{{- end }} diff --git a/charts/api-simulators-lab/values.yaml b/charts/api-simulators-lab/values.yaml new file mode 100644 index 0000000..798e1dd --- /dev/null +++ b/charts/api-simulators-lab/values.yaml @@ -0,0 +1,258 @@ +# Laboratory defaults — rotate secrets before any non-lab use. + +nameOverride: "" +fullnameOverride: "simulators" + +# Target Kubernetes namespace (also used by make helm up/down). +namespace: simulators +# Create the Namespace resource from this chart. +namespaceCreate: true + +imagePullSecrets: [] +ticketSigningKey: development-only-signing-key-change-me +logLevel: INFO +imageTag: latest + +postgres: + enabled: true + image: + repository: postgres + tag: 17.5-bookworm + pullPolicy: IfNotPresent + auth: + username: lab + password: lab + database: postgres + persistence: + enabled: true + size: 5Gi + storageClass: "" + resources: {} + # users/databases created on first start (empty volume only) + initDatabases: + - user: proxmox + password: proxmox + database: proxmox_simulator + - user: ovirt + password: ovirt + database: ovirt_simulator + - user: vmware + password: vmware + database: vmware_simulator + - user: openstack + password: openstack + database: openstack_simulator + +simulators: + proxmox: + enabled: true + image: + repository: inecs/proxmox-api-simulator + pullPolicy: IfNotPresent + port: 8006 + healthPath: /health/ready + replicas: 1 + db: + user: proxmox + password: proxmox + name: proxmox_simulator + env: + CONTRACT_SNAPSHOT: /app/contracts/pve-9.2.3.json + COMPATIBILITY_EVIDENCE: /app/evidence/pve-9.2.3.json + TASK_WORKER_CONCURRENCY: "2" + SIMULATION_TIME_SCALE: "10" + migrate: + enabled: true + command: ["python", "-m", "app.db.migrate_cli"] + seed: + enabled: true + # Skip seed Job when this query returns a row (already seeded). + skipIfSql: "SELECT 1 FROM nodes LIMIT 1" + command: ["python", "-m", "app.simulation.seed_cli"] + env: + SEED_PROFILE: small + resources: {} + + ovirt: + enabled: true + image: + repository: inecs/ovirt-api-simulator + pullPolicy: IfNotPresent + port: 8080 + healthPath: /health/ready + replicas: 1 + db: + user: ovirt + password: ovirt + name: ovirt_simulator + env: + OVIRT_SERIES: "4.5" + APP_PORT: "8080" + migrate: + enabled: true + command: ["python", "-m", "app.db.migrate_cli"] + seed: + enabled: true + skipIfSql: "SELECT 1 FROM ov_datacenters LIMIT 1" + command: ["python", "-m", "app.ovirt.seed_cli", "--profile", "minimal"] + resources: {} + + vmware: + enabled: true + image: + repository: inecs/vmware-api-simulator + pullPolicy: IfNotPresent + port: 8080 + healthPath: /health/ready + replicas: 1 + db: + user: vmware + password: vmware + name: vmware_simulator + env: + ENABLE_PVE_STUB: "false" + SEED_VSPHERE_PROFILE: small + APP_PORT: "8080" + TASK_WORKER_CONCURRENCY: "2" + SIMULATION_TIME_SCALE: "10" + migrate: + enabled: true + command: ["python", "-m", "app.db.migrate_cli"] + seed: + enabled: true + skipIfSql: "SELECT 1 FROM vsphere_objects LIMIT 1" + command: ["python", "-m", "app.simulation.seed_cli"] + env: + SEED_PROFILE: small + SEED_VSPHERE_PROFILE: small + resources: {} + + openstack: + enabled: true + image: + repository: inecs/openstack-api-simulator + pullPolicy: IfNotPresent + port: 8080 + healthPath: /health/ready + replicas: 1 + db: + user: openstack + password: openstack + name: openstack_simulator + env: + APP_PORT: "8080" + TASK_WORKER_CONCURRENCY: "2" + SIMULATION_TIME_SCALE: "10" + migrate: + enabled: true + command: ["python", "-m", "app.db.migrate_cli"] + seed: + enabled: true + skipIfSql: "SELECT 1 FROM os_projects LIMIT 1" + command: ["python", "-m", "app.openstack.seed_cli", "--profile", "minimal"] + resources: {} + +# Ingress hosts — edit here. Each enabled entry creates one Ingress. +# Default hosts: .lab.local — for Let's Encrypt use a public DNS name. +ingressClassName: nginx + +ingresses: + proxmox: + enabled: true + className: "" # falls back to ingressClassName + host: proxmox.lab.local + path: / + pathType: Prefix + service: proxmox + port: 8006 + tls: + - secretName: simulators-proxmox-tls + hosts: [proxmox.lab.local] + annotations: {} + + ovirt: + enabled: true + className: "" + host: ovirt.lab.local + path: / + pathType: Prefix + service: ovirt + port: 8080 + tls: + - secretName: simulators-ovirt-tls + hosts: [ovirt.lab.local] + annotations: {} + + vmware: + enabled: true + className: "" + host: vmware.lab.local + path: / + pathType: Prefix + service: vmware + port: 8080 + tls: + - secretName: simulators-vmware-tls + hosts: [vmware.lab.local] + annotations: + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header X-VMware-Service "vcenter"; + proxy_set_header X-Forwarded-Port "443"; + + openstack: + enabled: true + className: "" + host: openstack.lab.local + path: / + pathType: Prefix + service: openstack + port: 8080 + tls: + - secretName: simulators-openstack-tls + hosts: [openstack.lab.local] + annotations: + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header X-OpenStack-Service "https"; + proxy_set_header X-Forwarded-Port "443"; + + # Optional extras — same shape as above, e.g.: + # ovirt-ui: + # enabled: true + # host: ovirt-ui.lab.local + # service: ovirt + # port: 8080 + # tls: + # - secretName: simulators-ovirt-ui-tls + # hosts: [ovirt-ui.lab.local] + # openstack-keystone: + # enabled: true + # host: keystone.lab.local + # service: openstack + # port: 8080 + # tls: + # - secretName: simulators-keystone-tls + # hosts: [keystone.lab.local] + # annotations: + # nginx.ingress.kubernetes.io/configuration-snippet: | + # proxy_set_header X-OpenStack-Service "keystone"; + +# TLS via cert-manager + Let's Encrypt (HTTP-01). +# Requires cert-manager in the cluster. Hosts must resolve publicly for ACME. +certManager: + enabled: true + # Create ClusterIssuer resources from this chart (false = use existing letsencrypt-prod). + createClusterIssuer: false + createStagingIssuer: true + email: "" # required when createClusterIssuer=true + issuerName: letsencrypt-prod + server: https://acme-v02.api.letsencrypt.org/directory + stagingIssuerName: letsencrypt-staging + stagingServer: https://acme-staging-v02.api.letsencrypt.org/directory + useStaging: false + solverIngressClassName: nginx + +seedJobs: + # post-install / post-upgrade Jobs; skipIfSql avoids re-seeding when data exists + enabled: true + backoffLimit: 3 + ttlSecondsAfterFinished: 600 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..256e3ec --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,351 @@ +# Self-contained lab stack: Proxmox + oVirt + VMware + OpenStack +# One PostgreSQL, four databases. No sibling source trees required. +# +# cd /Users/inecs/Разработка/simulators +# docker compose pull +# docker compose up -d +# +# Override tags: IMAGE_TAG=0.1.0 docker compose up -d +# +# Laboratory only — default passwords / TICKET_SIGNING_KEY are intentional. + +name: api-simulators-lab + +x-ticket: &ticket + TICKET_SIGNING_KEY: ${TICKET_SIGNING_KEY:-development-only-signing-key-change-me} + LOG_LEVEL: ${LOG_LEVEL:-INFO} + +networks: + lab: + proxmox_net: + ovirt_net: + vmware_net: + openstack_net: + +volumes: + postgres-data: + +services: + postgres: + image: postgres:17.5-bookworm + restart: unless-stopped + networks: [lab, proxmox_net, ovirt_net, vmware_net, openstack_net] + environment: + POSTGRES_USER: lab + POSTGRES_PASSWORD: lab + POSTGRES_DB: postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready -U lab -d postgres"] + interval: 5s + timeout: 3s + retries: 12 + start_period: 5s + volumes: + - postgres-data:/var/lib/postgresql/data + - ./docker/postgres-init:/docker-entrypoint-initdb.d:ro + ports: + - "127.0.0.1:5432:5432" + + # ── Proxmox VE API ────────────────────────────────────── + proxmox-migrate: + profiles: [init] + image: inecs/proxmox-api-simulator:${IMAGE_TAG:-latest} + networks: [proxmox_net] + environment: + <<: *ticket + DATABASE_URL: postgresql://proxmox:proxmox@postgres:5432/proxmox_simulator + CONTRACT_SNAPSHOT: /app/contracts/pve-9.2.3.json + COMPATIBILITY_EVIDENCE: /app/evidence/pve-9.2.3.json + depends_on: + postgres: { condition: service_healthy } + entrypoint: ["python"] + command: ["-m", "app.db.migrate_cli"] + restart: "no" + + proxmox: + image: inecs/proxmox-api-simulator:${IMAGE_TAG:-latest} + restart: unless-stopped + networks: [proxmox_net] + environment: + <<: *ticket + DATABASE_URL: postgresql://proxmox:proxmox@postgres:5432/proxmox_simulator + CONTRACT_SNAPSHOT: /app/contracts/pve-9.2.3.json + COMPATIBILITY_EVIDENCE: /app/evidence/pve-9.2.3.json + TASK_WORKER_CONCURRENCY: "2" + SIMULATION_TIME_SCALE: "10" + depends_on: + postgres: { condition: service_healthy } + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8006/health/ready', timeout=2)", + ] + interval: 10s + timeout: 3s + retries: 10 + start_period: 25s + ports: + - "8006:8006" + + proxmox-seed: + profiles: [init] + image: inecs/proxmox-api-simulator:${IMAGE_TAG:-latest} + networks: [proxmox_net] + environment: + <<: *ticket + DATABASE_URL: postgresql://proxmox:proxmox@postgres:5432/proxmox_simulator + SEED_PROFILE: ${PROXMOX_SEED_PROFILE:-small} + depends_on: + proxmox: { condition: service_healthy } + entrypoint: ["python"] + command: ["-m", "app.simulation.seed_cli"] + restart: "no" + + # ── oVirt Engine API ──────────────────────────────────── + ovirt-migrate: + profiles: [init] + image: inecs/ovirt-api-simulator:${IMAGE_TAG:-latest} + networks: [ovirt_net] + environment: + <<: *ticket + DATABASE_URL: postgresql://ovirt:ovirt@postgres:5432/ovirt_simulator + OVIRT_SERIES: ${OVIRT_SERIES:-4.5} + depends_on: + postgres: { condition: service_healthy } + entrypoint: ["python"] + command: ["-m", "app.db.migrate_cli"] + restart: "no" + + ovirt: + image: inecs/ovirt-api-simulator:${IMAGE_TAG:-latest} + restart: unless-stopped + networks: + ovirt_net: + aliases: [simulator] + environment: + <<: *ticket + DATABASE_URL: postgresql://ovirt:ovirt@postgres:5432/ovirt_simulator + OVIRT_SERIES: ${OVIRT_SERIES:-4.5} + APP_PORT: "8080" + depends_on: + postgres: { condition: service_healthy } + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/ready', timeout=2)", + ] + interval: 10s + timeout: 3s + retries: 10 + start_period: 30s + expose: + - "8080" + + ovirt-gateway: + image: nginx:1.28.0-alpine + restart: unless-stopped + networks: [ovirt_net] + depends_on: + ovirt: { condition: service_healthy } + ports: + - "7443:443" + - "7500:5000" + volumes: + - ./docker/ovirt/ovirt-engine.conf:/etc/nginx/conf.d/default.conf:ro + - ./docker/tls/server.crt:/etc/nginx/tls/server.crt:ro + - ./docker/tls/server.key:/etc/nginx/tls/server.key:ro + read_only: true + tmpfs: [/var/cache/nginx, /var/run, /tmp] + security_opt: [no-new-privileges:true] + + ovirt-seed: + profiles: [init] + image: inecs/ovirt-api-simulator:${IMAGE_TAG:-latest} + networks: [ovirt_net] + environment: + <<: *ticket + DATABASE_URL: postgresql://ovirt:ovirt@postgres:5432/ovirt_simulator + OVIRT_SERIES: ${OVIRT_SERIES:-4.5} + depends_on: + ovirt: { condition: service_healthy } + entrypoint: ["python"] + command: ["-m", "app.ovirt.seed_cli", "--profile", "${OVIRT_SEED_PROFILE:-minimal}"] + restart: "no" + + # ── VMware vSphere API ────────────────────────────────── + vmware-migrate: + profiles: [init] + image: inecs/vmware-api-simulator:${IMAGE_TAG:-latest} + networks: [vmware_net] + environment: + <<: *ticket + DATABASE_URL: postgresql://vmware:vmware@postgres:5432/vmware_simulator + ENABLE_PVE_STUB: "false" + APP_PORT: "8080" + depends_on: + postgres: { condition: service_healthy } + entrypoint: ["python"] + command: ["-m", "app.db.migrate_cli"] + restart: "no" + + vmware: + image: inecs/vmware-api-simulator:${IMAGE_TAG:-latest} + restart: unless-stopped + networks: + vmware_net: + aliases: [simulator] + environment: + <<: *ticket + DATABASE_URL: postgresql://vmware:vmware@postgres:5432/vmware_simulator + ENABLE_PVE_STUB: "false" + SEED_VSPHERE_PROFILE: ${VMWARE_SEED_PROFILE:-small} + APP_PORT: "8080" + TASK_WORKER_CONCURRENCY: "2" + SIMULATION_TIME_SCALE: "10" + depends_on: + postgres: { condition: service_healthy } + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/ready', timeout=2)", + ] + interval: 10s + timeout: 3s + retries: 10 + start_period: 25s + expose: + - "8080" + + vmware-gateway: + image: nginx:1.28.0-alpine + restart: unless-stopped + networks: [vmware_net] + depends_on: + vmware: { condition: service_healthy } + ports: + - "8081:80" + - "8443:443" + volumes: + - ./docker/vmware/vmware-ports.conf:/etc/nginx/conf.d/default.conf:ro + - ./docker/tls/server.crt:/etc/nginx/tls/server.crt:ro + - ./docker/tls/server.key:/etc/nginx/tls/server.key:ro + read_only: true + tmpfs: [/var/cache/nginx, /var/run, /tmp] + security_opt: [no-new-privileges:true] + + vmware-seed: + profiles: [init] + image: inecs/vmware-api-simulator:${IMAGE_TAG:-latest} + networks: [vmware_net] + environment: + <<: *ticket + DATABASE_URL: postgresql://vmware:vmware@postgres:5432/vmware_simulator + ENABLE_PVE_STUB: "false" + SEED_PROFILE: ${VMWARE_SEED_PROFILE:-small} + SEED_VSPHERE_PROFILE: ${VMWARE_SEED_PROFILE:-small} + depends_on: + vmware: { condition: service_healthy } + entrypoint: ["python"] + command: ["-m", "app.simulation.seed_cli"] + restart: "no" + + # ── OpenStack API ─────────────────────────────────────── + openstack-migrate: + profiles: [init] + image: inecs/openstack-api-simulator:${IMAGE_TAG:-latest} + networks: [openstack_net] + environment: + <<: *ticket + DATABASE_URL: postgresql://openstack:openstack@postgres:5432/openstack_simulator + APP_PORT: "8080" + depends_on: + postgres: { condition: service_healthy } + entrypoint: ["python"] + command: ["-m", "app.db.migrate_cli"] + restart: "no" + + openstack: + image: inecs/openstack-api-simulator:${IMAGE_TAG:-latest} + restart: unless-stopped + networks: + openstack_net: + aliases: [simulator] + environment: + <<: *ticket + DATABASE_URL: postgresql://openstack:openstack@postgres:5432/openstack_simulator + APP_PORT: "8080" + TASK_WORKER_CONCURRENCY: "2" + SIMULATION_TIME_SCALE: "10" + depends_on: + postgres: { condition: service_healthy } + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/ready', timeout=2)", + ] + interval: 10s + timeout: 3s + retries: 10 + start_period: 25s + expose: + - "8080" + + openstack-gateway: + image: nginx:1.28.0-alpine + restart: unless-stopped + networks: [openstack_net] + depends_on: + openstack: { condition: service_healthy } + ports: + - "9080:80" + - "9443:443" + - "9500:5000" + - "5050:5050" + - "6385:6385" + - "8000:8000" + - "8003:8003" + - "8004:8004" + - "8042:8042" + - "8080:8080" + - "8774:8774" + - "8776:8776" + - "8779:8779" + - "8786:8786" + - "8989:8989" + - "9001:9001" + - "9090:9090" + - "9292:9292" + - "9696:9696" + - "9876:9876" + volumes: + - ./docker/openstack/openstack-ports.conf:/etc/nginx/conf.d/default.conf:ro + - ./docker/tls/server.crt:/etc/nginx/tls/server.crt:ro + - ./docker/tls/server.key:/etc/nginx/tls/server.key:ro + read_only: true + tmpfs: [/var/cache/nginx, /var/run, /tmp] + security_opt: [no-new-privileges:true] + + openstack-seed: + profiles: [init] + image: inecs/openstack-api-simulator:${IMAGE_TAG:-latest} + networks: [openstack_net] + environment: + <<: *ticket + DATABASE_URL: postgresql://openstack:openstack@postgres:5432/openstack_simulator + depends_on: + openstack: { condition: service_healthy } + entrypoint: ["python"] + command: ["-m", "app.openstack.seed_cli", "--profile", "${OPENSTACK_SEED_PROFILE:-minimal}"] + restart: "no" diff --git a/docker/openstack/openstack-ports.conf b/docker/openstack/openstack-ports.conf new file mode 100644 index 0000000..c7dd9b8 --- /dev/null +++ b/docker/openstack/openstack-ports.conf @@ -0,0 +1,114 @@ +# Auto-generated from app.openstack.surface.SERVICES +upstream openstack_simulator { + server simulator:8080; +} + +map $server_port $openstack_service { + default "simulator"; + 5000 "keystone"; + 8774 "nova"; + 9696 "neutron"; + 9292 "glance"; + 8776 "cinder"; + 8003 "placement"; + 8004 "heat"; + 8000 "heat-cfn"; + 8080 "swift"; + 6385 "ironic"; + 9876 "octavia"; + 9311 "barbican"; + 8786 "manila"; + 9001 "designate"; + 9511 "magnum"; + 9517 "zun"; + 8779 "trove"; + 8989 "mistral"; + 8042 "aodh"; + 8889 "cloudkitty"; + 9090 "freezer"; + 1234 "blazar"; + 8999 "vitrage"; + 15868 "masakari"; + 9890 "tacker"; + 5050 "adjutant"; + 9322 "watcher"; + 8888 "zaqar"; + 80 "horizon"; + 443 "https"; +} + +server { + listen 5000; # keystone (identity) + listen 8774; # nova (compute) + listen 9696; # neutron (network) + listen 9292; # glance (image) + listen 8776; # cinder (volumev3) + listen 8003; # placement (placement) + listen 8004; # heat (orchestration) + listen 8000; # heat-cfn (cloudformation) + listen 8080; # swift (object-store) + listen 6385; # ironic (baremetal) + listen 9876; # octavia (load-balancer) + listen 9311; # barbican (key-manager) + listen 8786; # manila (sharev2) + listen 9001; # designate (dns) + listen 9511; # magnum (container-infra) + listen 9517; # zun (container) + listen 8779; # trove (database) + listen 8989; # mistral (workflowv2) + listen 8042; # aodh (alarming) + listen 8889; # cloudkitty (rating) + listen 9090; # freezer (backup) + listen 1234; # blazar (reservation) + listen 8999; # vitrage (rca) + listen 15868; # masakari (instance-ha) + listen 9890; # tacker (nfv-orchestration) + listen 5050; # adjutant (admin-logic) + listen 9322; # watcher (infra-optim) + listen 8888; # zaqar (messaging) + listen 80; + + server_name _; + resolver 127.0.0.11 valid=10s ipv6=off; + + location / { + proxy_pass http://openstack_simulator; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-OpenStack-Service $openstack_service; + # Console on :5000 may target another service via relative /v2.1|/v2.0|… paths. + proxy_set_header X-OpenStack-Route-Service $http_x_openstack_route_service; + proxy_set_header X-Request-ID $request_id; + proxy_set_header OpenStack-API-Version $http_openstack_api_version; + proxy_set_header X-OpenStack-Nova-API-Version $http_x_openstack_nova_api_version; + add_header X-OpenStack-Service $openstack_service always; + add_header Access-Control-Expose-Headers "X-Subject-Token,x-subject-token" always; + add_header X-Forwarded-Port $server_port always; + } +} + +server { + listen 443 ssl; + server_name _; + ssl_certificate /etc/nginx/tls/server.crt; + ssl_certificate_key /etc/nginx/tls/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + resolver 127.0.0.11 valid=10s ipv6=off; + location / { + proxy_pass http://openstack_simulator; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Port 443; + proxy_set_header X-OpenStack-Service https; + proxy_set_header X-Request-ID $request_id; + add_header X-OpenStack-Service https always; + add_header X-Forwarded-Port 443 always; + } +} diff --git a/docker/ovirt/ovirt-engine.conf b/docker/ovirt/ovirt-engine.conf new file mode 100644 index 0000000..6df4a24 --- /dev/null +++ b/docker/ovirt/ovirt-engine.conf @@ -0,0 +1,49 @@ +# nginx gateway — only two public listeners: +# :443 Engine API + SSO (/ovirt-engine/…) +# :5000 Web UI console +# Upstream FastAPI listens on simulator:8080. + +upstream ovirt_simulator { + server simulator:8080; + keepalive 32; +} + +# --- Engine HTTPS (API + SSO) --- +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name _; + + ssl_certificate /etc/nginx/tls/server.crt; + ssl_certificate_key /etc/nginx/tls/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + + client_max_body_size 64m; + + location / { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Port 443; + proxy_pass http://ovirt_simulator; + } +} + +# --- Simulator Web UI console --- +server { + listen 5000; + listen [::]:5000; + server_name _; + + location / { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port 5000; + proxy_pass http://ovirt_simulator; + } +} diff --git a/docker/postgres-init/01-simulators.sql b/docker/postgres-init/01-simulators.sql new file mode 100644 index 0000000..ae760ee --- /dev/null +++ b/docker/postgres-init/01-simulators.sql @@ -0,0 +1,11 @@ +CREATE USER proxmox WITH PASSWORD 'proxmox'; +CREATE DATABASE proxmox_simulator OWNER proxmox; + +CREATE USER ovirt WITH PASSWORD 'ovirt'; +CREATE DATABASE ovirt_simulator OWNER ovirt; + +CREATE USER vmware WITH PASSWORD 'vmware'; +CREATE DATABASE vmware_simulator OWNER vmware; + +CREATE USER openstack WITH PASSWORD 'openstack'; +CREATE DATABASE openstack_simulator OWNER openstack; diff --git a/docker/tls/server.crt b/docker/tls/server.crt new file mode 100644 index 0000000..bce557b --- /dev/null +++ b/docker/tls/server.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICyTCCAbGgAwIBAgIJAIbJhnhVx8uWMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0yNjA3MTIyMTQyNTFaFw0zNjA3MDkyMTQyNTFaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALscj7/1WDybjz8x01EvUFVemov6zkezOwfsOXKVyEOnOTxPjWruzDYnB8y6 +NH/5PojUns7GB1kuRhZWUXGY0FG/sSgF0X9nwEHoby8ekju2F55NUzzpu9BfM2AU +S17S8h5Oxc4Qi6d9RoeRG25YmMywPCyp2SMnuu14w55KTAt7Ir7mbTAv8ZIMbVhq +34tH45ONQvGftN4JNvwZr7Uf+EuupWsnILfkz1Cw1cj88adDZHwxE7Hkx7TiQP6o +DPDeg+XYH0vB2HR25JSP9z0uyeeF6n6cExgfwVZy2una7jQp887N5xLgTUGlnmFM +y1z2AO2+Mw1Lh2UC/OQrp9T1ztMCAwEAAaMeMBwwGgYDVR0RBBMwEYIJbG9jYWxo +b3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCICPRCT+m+EKHkaWG2eY2AqQ7a +24Bd60ZsZxJNAloXAd1X8cedz5yq0rm9pqF5Fq883dysgCVSDylwqy4YzllhTWsy ++M3TE85ZyKKi6S7kR7Z0Exf0I4S7G9zTtrzEXn9kco1q5g/jE7aQi2E2z5poaIg+ +TlUCq5IePsS6gZCvzXPgU1mJ5dQFlqsOW6Lk1mOCjmKT2SaF4eL2hleatqHv667c +fJWYLotjAJoVQKrjItGeHXPosZEW5g17gFD88XZRMlUx5xN5/ioaKmLiyI28aNF3 +nNaukGC/N5fPsgghb0wkYmPHh+dFE/1uMIq0cOzNcyG6ZQkQzikiYhytWsOu +-----END CERTIFICATE----- diff --git a/docker/tls/server.key b/docker/tls/server.key new file mode 100644 index 0000000..5cc6910 --- /dev/null +++ b/docker/tls/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7HI+/9Vg8m48/ +MdNRL1BVXpqL+s5HszsH7DlylchDpzk8T41q7sw2JwfMujR/+T6I1J7OxgdZLkYW +VlFxmNBRv7EoBdF/Z8BB6G8vHpI7theeTVM86bvQXzNgFEte0vIeTsXOEIunfUaH +kRtuWJjMsDwsqdkjJ7rteMOeSkwLeyK+5m0wL/GSDG1Yat+LR+OTjULxn7TeCTb8 +Ga+1H/hLrqVrJyC35M9QsNXI/PGnQ2R8MROx5Me04kD+qAzw3oPl2B9Lwdh0duSU +j/c9Lsnnhep+nBMYH8FWctrp2u40KfPOzecS4E1BpZ5hTMtc9gDtvjMNS4dlAvzk +K6fU9c7TAgMBAAECggEBAINmo2zjF3w4onh2vTgeSgQp087J62Ne8u21bwKRPXqF +TSSVmXKnELJW5ptXiNb2anwdFQmQ+EggvwegxsFH18QRIpBAxcb7TYD7gllM1tUo +I54AH5x/aG4E7Udj+So2aeHu3+q+o9STnZxGw0TS4zub6CZVgS+3DwcF8BqRgqXs +NuDIIJWosuchbb3DdlPygRajiN2teJtNfw9rcLfC4BY5i4y/H7RMpklM5VkTXiGc +NxyG4qkdHP0jlL9Z9wRa859uYeb7kVm+vhfgUXMbiRn9FcxrROOpPIwTAv76Y5nu +4EF/s0TPC+ei7hjCpN1WK2/n6dgiVYBVBUpWHalZBIECgYEA7+E2T5PlKuT7Kugs +qx+CHvZXm2hZ9NVDYS6gNAZt6kr5enbb9rzCF/U+jx14COPyGcoJeDOUW1yZGTgH +98JkEEHB6fgSPAU3pp2aMslMRNTZqfM0vL+BRpJT+fPbI9y8WpMzm/NpmnZZK8rh +xLbg+xAa7iMltscCcY2uD8NmhKkCgYEAx6+Ma5WQ0Enmju+XUANrOuKDN9aTXXc6 +iqlqtXfadc/Lc6E+lzSxRm5t95t+6AX2mYsNOWsuWRCBqHMFDxg31moYEeBQvQW9 +kwJQ5JsmOSCzMfDPUrQHihaq9xwxhoBxJXIRs3JlYm/nty8LO959R1V0IrHsPznH +BVs7pbAo6RsCgYEAyvK4t3UCI1tdoPyTpiffOADlN+d+jCTOf+8pvTpfTiUmk1Ty +XvtuH0TvK7gb8TGhh+4mOtswvmdGZE7CdvyxGgv4WtH142/qmH2okyU58NZAXYgV +a0d+wU1V3RhSpDHB7cOym1PCWdudL+7TOlIbYG5MyoNUCiKvT5E13cJM/xkCgYAC +WWNahKjuemAXAGSUUWX6jF2k04ZqTBPJO9MAjYdpaWdoVdZJqxoGzRfIGPE2Q5Oy +HLusGEG0VIhh9fByTAOkJx1fYHcyshWX3CgdeGHLvEG/bajSvUF1c2zReWhvv6UV +HrFsngTpUo20Tv5f1u88Xpn+Kn+wArr/qiIagecJTwKBgDPEa71fqt7WyjHCNuIm +hJeBCIjTZ8N1Jk0GUHyucbFPARWxYcn0zRTwHOXnXt+Z6GvAfuS7YElSXYVjw2Uy +wUVD+7zh0ydkWC1HJPnjalmHHVpv1RFNEJGAYQ8Vxd6G2EoY4ZFByZomWTxfXrvq +Dr9hsGtmZu1knNwfrOu2kyB5 +-----END PRIVATE KEY----- diff --git a/docker/vmware/vmware-ports.conf b/docker/vmware/vmware-ports.conf new file mode 100644 index 0000000..1a9cd33 --- /dev/null +++ b/docker/vmware/vmware-ports.conf @@ -0,0 +1,62 @@ +# Lab gateway that exposes vCenter default HTTPS (443) and proxies to the +# single FastAPI simulator process. +# +# Upstream listens on an internal-only port (simulator:8080). Host clients +# should hit https://localhost/ — not the internal app port. +# +# Use a variable + Docker DNS resolver so nginx starts even if the simulator +# hostname is not yet resolvable (avoids crash-loop on recreate). + +map $server_port $vmware_service { + default "simulator"; + 80 "http"; + 443 "vcenter"; +} + +server { + listen 80; + server_name _; + resolver 127.0.0.11 valid=10s ipv6=off; + + set $vmware_upstream simulator:8080; + + location / { + proxy_pass http://$vmware_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-VMware-Service $vmware_service; + proxy_set_header X-Request-ID $request_id; + add_header X-VMware-Service $vmware_service always; + add_header X-Forwarded-Port $server_port always; + } +} + +server { + listen 443 ssl; + server_name _; + + ssl_certificate /etc/nginx/tls/server.crt; + ssl_certificate_key /etc/nginx/tls/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + + resolver 127.0.0.11 valid=10s ipv6=off; + set $vmware_upstream simulator:8080; + + location / { + proxy_pass http://$vmware_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Port 443; + proxy_set_header X-VMware-Service vcenter; + proxy_set_header X-Request-ID $request_id; + add_header X-VMware-Service vcenter always; + add_header X-Forwarded-Port 443 always; + } +}