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.
This commit is contained in:
2026-07-18 11:52:33 +03:00
commit a3ce2d473c
24 changed files with 1960 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
.env
+182
View File
@@ -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 </dev/tty); \
if [ -z "$$msg" ]; then echo "Empty commit message, aborting." >&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
+162
View File
@@ -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).
+163
View File
@@ -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).
+3
View File
@@ -0,0 +1,3 @@
.DS_Store
*.tgz
charts/
+14
View File
@@ -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
@@ -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`.
@@ -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 }}
@@ -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 }}
@@ -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 }}
@@ -0,0 +1,8 @@
{{- if .Values.namespaceCreate }}
apiVersion: v1
kind: Namespace
metadata:
name: {{ include "lab.namespace" . }}
labels:
{{- include "lab.labels" . | nindent 4 }}
{{- end }}
@@ -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 }}
@@ -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 }}
@@ -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 }}
@@ -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 }}
@@ -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 }}
+258
View File
@@ -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: <name>.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
+351
View File
@@ -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"
+114
View File
@@ -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;
}
}
+49
View File
@@ -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;
}
}
+11
View File
@@ -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;
+17
View File
@@ -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-----
+28
View File
@@ -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-----
+62
View File
@@ -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;
}
}