diff --git a/create-a-container/Makefile b/create-a-container/Makefile index 8888c403..c7523dc9 100644 --- a/create-a-container/Makefile +++ b/create-a-container/Makefile @@ -53,9 +53,10 @@ build: deps # --no-package-lock`. This is a fast, incremental install that keeps # devDependencies and runs the server's patch-package postinstall, but never # rewrites package-lock.json (so `make dev` leaves the lockfile untouched even -# when the local npm would otherwise renormalize it). Then run migrations, seed -# a localhost site + dummy node, build the client once, and run three processes -# together via concurrently: +# when the local npm would otherwise renormalize it). Then run migrations (which +# also runs the dev-only seeders, e.g. a localhost site + dummy node, via +# `db:seed:all`), build the client once, and run three processes together via +# concurrently: # - server (nodemon, restarts on change) # - job-runner (so container creation exercises the real POST /containers -> # Job -> job-runner -> bin/create-container.js -> DummyApi path) @@ -72,7 +73,6 @@ dev: npm install --no-package-lock npm --prefix client install --no-package-lock npm run db:migrate - node bin/dev-bootstrap.js npm --prefix client run build @echo "Starting Manager (server + job-runner + client watch) at http://localhost:3000 ..." $(LOG_LEVEL_PREFIX)npx concurrently -n server,jobs,client -c blue,magenta,green \ diff --git a/create-a-container/README.md b/create-a-container/README.md index a9ba5832..cedcdbb8 100644 --- a/create-a-container/README.md +++ b/create-a-container/README.md @@ -1,639 +1,122 @@ -# Create-a-Container +# Create-a-Container (Manager) -A web application for managing LXC container creation, configuration, and lifecycle on Proxmox VE infrastructure. Provides a user-friendly interface and REST API for container management with automated database tracking and nginx reverse proxy configuration generation. +The Manager web application for opensource-server: a Node.js + Express + Sequelize +app that manages LXC container creation, configuration, and lifecycle on Proxmox +VE, exposes a REST API, and generates the nginx/dnsmasq configuration consumed by +agents. -## Data Model +This README documents the component itself. For installing, operating, or +developing the wider system, start with the guides below. -```mermaid -erDiagram - Node ||--o{ Container : "hosts" - Container ||--o{ Service : "exposes" - - Node { - int id PK - string name UK "Proxmox node name" - string apiUrl "Proxmox API URL" - boolean tlsVerify "Verify TLS certificates" - datetime createdAt - datetime updatedAt - } - - Container { - int id PK - string hostname UK "FQDN hostname" - string username "Owner username" - string template "Template name" - int creationJobId FK "References Job" - int nodeId FK "References Node" - int containerId UK "Proxmox VMID" - string macAddress UK "MAC address (nullable)" - string ipv4Address UK "IPv4 address (nullable)" - string aiContainer "Node type flag" - datetime createdAt - datetime updatedAt - } - - Service { - int id PK - int containerId FK "References Container" - enum type "tcp, udp, or http" - int internalPort "Port inside container" - int externalPort "External port (tcp/udp only)" - boolean tls "TLS enabled (tcp only)" - string externalHostname UK "Public hostname (http only)" - datetime createdAt - datetime updatedAt - } -``` - -**Key Constraints:** -- `(Node.name)` - Unique -- `(Container.hostname)` - Unique -- `(Container.nodeId, Container.containerId)` - Unique (same VMID can exist on different nodes) -- `(Service.externalHostname)` - Unique when type='http' -- `(Service.type, Service.externalPort)` - Unique when type='tcp' or type='udp' - -## Features - -- **User Authentication** - Proxmox VE authentication integration -- **Container Management** - Create, list, and track LXC containers -- **Docker/OCI Support** - Pull and deploy containers from Docker Hub, GHCR, or any OCI registry -- **Service Registry** - Track HTTP/TCP/UDP services running on containers -- **Dynamic Nginx Config** - Generate nginx reverse proxy configurations on-demand -- **Real-time Progress** - SSE (Server-Sent Events) for container creation progress -- **User Registration** - Self-service account request system with email notifications -- **Rate Limiting** - Protection against abuse (100 requests per 15 minutes) - -## Prerequisites - -### System Requirements -- **Node.js** 18.x or higher -- **PostgreSQL** 16 or higher -- **Proxmox VE** cluster with API access -- **SMTP server** for email notifications (optional) - -### Services -```bash -# Install Node.js (Debian/Ubuntu) -curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - -sudo apt-get install -y nodejs - -# Install PostgreSQL -sudo apt-get install postgresql -y -``` +| If you want to... | Read | +|---|---| +| Install and operate a production deployment | [Installation Guide](../mie-opensource-landing/docs/admins/installation.md) | +| Run the Manager locally to develop or contribute | [Development Workflow](../mie-opensource-landing/docs/developers/development-workflow.md) | +| Understand the system design | [System Architecture](../mie-opensource-landing/docs/developers/system-architecture.md) | -## Installation +## Running it -### 1. Clone Repository -```bash -cd /opt -sudo git clone https://github.com/mieweb/opensource-server.git -cd opensource-server/create-a-container -``` - -### 2. Install Dependencies -```bash -npm install -``` +### Development -### 3. Database Setup - -#### Create Database and User -```sql -CREATE DATABASE opensource_containers CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE USER 'container_manager'@'localhost' IDENTIFIED BY 'secure_password_here'; -GRANT ALL PRIVILEGES ON opensource_containers.* TO 'container_manager'@'localhost'; -FLUSH PRIVILEGES; -``` - -#### Run Migrations ```bash -npm run db:migrate +make dev ``` -This creates the following tables: -- `Containers` - Container records (hostname, IP, MAC, OS, etc.) -- `Services` - Service mappings (ports, protocols, hostnames) - -### 4. Configuration +That's all you need. `make dev` installs dependencies, runs database migrations +and dev seeders, builds the client, and starts the server, the job-runner, and +the client build watcher together. It uses SQLite and a dummy (mock) hypervisor, +so **no `.env`, PostgreSQL, or Proxmox cluster is required** — the Manager comes +up at and can "create" containers locally (simulated). -Create a `.env` file in the `create-a-container` directory: - -```bash -# Database Configuration -POSTGRES_HOST=localhost -POSTGRES_USER=cluster_manager -POSTGRES_PASSWORD=secure_password_here -POSTGRES_DATABASE=cluster_manager -DATABASE_DIALECT=postgres - -# Session Configuration -SESSION_SECRET=generate_random_secret_here - -# Application -NODE_ENV=production -``` - -#### Generate Session Secret -```bash -node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" -``` - -### 5. Start Application - -#### Development Mode (with auto-reload) -```bash -npm run dev -``` - -#### Production Mode -```bash -node server.js -``` - -#### As a System Service -Create `/etc/systemd/system/create-a-container.service`: -```ini -[Unit] -Description=Create-a-Container Service -After=network.target mariadb.service - -[Service] -Type=simple -User=www-data -WorkingDirectory=/opt/opensource-server/create-a-container -Environment=NODE_ENV=production -ExecStart=/usr/bin/node server.js -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target -``` +Pass `LOG_LEVEL=trace` to additionally log every SQL query: -Enable and start: ```bash -sudo systemctl daemon-reload -sudo systemctl enable create-a-container -sudo systemctl start create-a-container -sudo systemctl status create-a-container +make dev LOG_LEVEL=trace ``` -## API Routes +For the full Docker-based stack (real reverse proxy, DNS, Proxmox VE), see the +[Development Workflow](../mie-opensource-landing/docs/developers/development-workflow.md). -### Authentication Routes +### Production -#### `GET /login` -Display login page +The Manager is not installed by hand in production. It ships as: -#### `POST /login` -Authenticate user with Proxmox VE credentials -- **Body**: `{ username, password }` -- **Returns**: `{ success: true, redirect: "/" }` +- an **OCI image** (`images/manager`) — the supported deployment, used by the + [Installation Guide](../mie-opensource-landing/docs/admins/installation.md); and +- distribution **packages** built from this directory with `make deb`, `make rpm`, + or `make apk` (via [fpm](https://fpm.readthedocs.io/)), which install the app + under `/opt/opensource-server/create-a-container` and register the + `container-creator` and `job-runner` systemd services. -#### `POST /logout` -End user session +In both cases the app runs `server.js` (HTTP API + UI) and `job-runner.js` +(background worker). Database connection settings come from the environment (see +[Configuration](#configuration)); the manager image provisions PostgreSQL and +writes these to `/etc/default/container-creator` on first boot. -### Container Management Routes +## Configuration -#### `GET /` -Redirect to `/containers` +Configuration is read from environment variables (a local `.env` file is +supported in development). See [`example.env`](example.env) for the full list, +including the database dialect/connection settings and the optional OIDC +single-sign-on variables. Session secrets are generated and stored automatically +— there is no secret to configure. -#### `GET /containers` (Auth Required) -List all containers for authenticated user -- **Returns**: HTML page with container list +Production uses PostgreSQL (`DATABASE_DIALECT=postgres`); `make dev` uses SQLite. -#### `GET /containers/new` (Auth Required) -Display container creation form +## Database migrations & seeders -#### `POST /containers` -Create a container asynchronously via a background job -- **Body**: `{ hostname, template, customTemplate, services }` where: - - `hostname`: Container hostname - - `template`: Template selection in format "nodeName,vmid" OR "custom" for Docker images - - `customTemplate`: Docker image reference when template="custom" (e.g., `nginx`, `nginx:alpine`, `myorg/myapp:v1`, `ghcr.io/org/image:tag`) - - `services`: Object of service definitions -- **Returns**: Redirect to containers list with flash message -- **Process**: Creates pending container, services, and job in a single transaction. Docker image references are normalized to full format (`host/org/image:tag`). The job-runner executes the actual Proxmox operations. +Migrations are applied **automatically at server startup**. `server.js` calls the +runner in `utils/migrate.js`, which uses [umzug](https://github.com/sequelize/umzug) +to run every pending migration in `migrations/` before the HTTP server begins +listening. Only one process migrates at a time: the run is wrapped in an +engine-appropriate advisory lock (`pg_advisory_lock` on PostgreSQL, `GET_LOCK` on +MySQL; SQLite needs none). If a migration fails the process exits non-zero and +does not serve traffic. -#### `DELETE /containers/:id` (Auth Required) -Delete a container from both Proxmox and the database -- **Path Parameter**: `id` - Container database ID -- **Authorization**: User can only delete their own containers -- **Process**: - 1. Verifies container ownership - 2. Deletes container from Proxmox via API - 3. On success, removes container record from database (cascades to services) -- **Returns**: `{ success: true, message: "Container deleted successfully" }` -- **Errors**: - - `404` - Container not found - - `403` - User doesn't own the container - - `500` - Proxmox API deletion failed or node not configured +`sequelize-cli` is a dev dependency used for authoring and ad-hoc management: -#### Container status (`status` field) -Every container returned by the list, show, and create endpoints includes a -**live** `status` field, computed on demand rather than read from a stored -column. It is resolved by combining the container's run-state in Proxmox (from a -single per-node cluster snapshot) with the state of its create job. Possible -values: -- `running` — online in Proxmox -- `offline` — exists in Proxmox but stopped -- `creating` — no Proxmox VM yet, active create job -- `failed` — no Proxmox VM, create job failed -- `missing` — no Proxmox VM, create succeeded or no create job found -- `unknown` — Proxmox unreachable / node has no API credentials - -The create endpoint (`POST /containers`) returns `creating` immediately, since a -create job is enqueued and there is no Proxmox VM yet. - -#### `GET /status/:jobId` (Auth Required) -View container creation progress page - -#### `GET /api/stream/:jobId` -SSE stream for real-time container creation progress -- **Returns**: Server-Sent Events stream - -### Job Runner & Jobs API Routes - -#### `POST /jobs` (Admin Auth Required) -Enqueue a job for background execution -- **Body**: `{ "command": "" }` -- **Response**: `201 { id, status }` -- **Authorization**: Admin only (prevents arbitrary command execution) -- **Behavior**: Admin's username is recorded in `createdBy` column for audit trail - -#### `GET /jobs/:id` (Auth Required) -Fetch job metadata (command, status, timestamps) -- **Response**: `{ id, command, status, createdAt, updatedAt, createdBy }` -- **Authorization**: Only the job owner or admins may view -- **Returns**: `404` if unauthorized (prevents information leakage) - -#### `GET /jobs/:id/status` (Auth Required) -Fetch job output rows with offset/limit pagination -- **Query Params**: - - `offset` (optional, default 0) - Skip first N rows - - `limit` (optional, max 1000) - Return up to N rows -- **Response**: Array of JobStatus objects `[{ id, jobId, output, createdAt, updatedAt }, ...]` -- **Authorization**: Only the job owner or admins may view -- **Returns**: `404` if unauthorized - -### Job Runner System - -#### Background Job Execution -The job runner (`job-runner.js`) is a background Node.js process that: -1. Polls the `Jobs` table for `pending` status records -2. Claims a job transactionally (sets status to `running` and acquires row lock) -3. Spawns the job command in a shell subprocess -4. Streams stdout/stderr into the `JobStatuses` table in real-time -5. Updates job status to `success` or `failure` on process exit -6. Gracefully cancels running jobs on shutdown (SIGTERM/SIGINT) and marks them `cancelled` - -#### Data Models - -**Job Model** (`models/job.js`) -``` -id INT PRIMARY KEY AUTO_INCREMENT -command VARCHAR(2000) NOT NULL - shell command to execute -createdBy VARCHAR(255) - username of admin who enqueued (nullable for legacy jobs) -status ENUM('pending', 'running', 'success', 'failure', 'cancelled') -createdAt DATETIME -updatedAt DATETIME -``` - -**JobStatus Model** (`models/jobstatus.js`) -``` -id INT PRIMARY KEY AUTO_INCREMENT -jobId INT NOT NULL (FK → Jobs.id, CASCADE delete) -output TEXT - chunk of stdout/stderr from the job -createdAt DATETIME -updatedAt DATETIME -``` - -**Migrations** -- `migrations/20251117120000-create-jobs.js` -- `migrations/20251117120001-create-jobstatuses.js` (includes `updatedAt`) -- `migrations/20251117120002-add-job-createdby.js` (adds nullable `createdBy` column + index) - -#### Running the Job Runner - -**Development (foreground, logs to stdout)** ```bash -cd create-a-container -npm run job-runner -``` - -**Production (systemd service)** -Copy `systemd/job-runner.service` to `/etc/systemd/system/job-runner.service`: -```bash -sudo cp systemd/job-runner.service /etc/systemd/system/ -sudo systemctl daemon-reload -sudo systemctl enable --now job-runner.service -sudo systemctl status job-runner.service -``` - -#### Configuration - -**Database** (via `.env`) -- `POSTGRES_HOST`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DATABASE`, `DATABASE_DIALECT` - -**Runner Behavior** (environment variables) -- `JOB_RUNNER_POLL_MS` (default 2000) - Polling interval in milliseconds -- `JOB_RUNNER_CWD` (default cwd) - Working directory for spawned commands -- `NODE_ENV=production` - Recommended for production - -**Systemd Setup** (recommended for production) -Create `/etc/default/container-creator` with DB credentials: -```bash -POSTGRES_HOST=localhost -POSTGRES_USER=cluster_manager -POSTGRES_PASSWORD=secure_password_here -POSTGRES_DATABASE=cluster_manager -DATABASE_DIALECT=postgres -``` - -Update `job-runner.service` to include: -```ini -EnvironmentFile=/etc/default/container-creator -``` - -#### Security Considerations - -1. **Command Injection Risk**: The runner spawns commands via shell. Only admins can enqueue jobs via the API. Do not expose `POST /jobs` to untrusted users. -2. **Job Ownership**: Jobs are scoped by `createdBy`. Only the admin who created the job (or other admins) can view its metadata and output. Non-owners receive `404` (not `403`) to prevent information leakage. -3. **Legacy Jobs**: Jobs created before the `createdBy` migration will have `createdBy = NULL` and are visible only to admins. -4. **Graceful Shutdown**: On SIGTERM/SIGINT, the runner kills all running child processes and marks their jobs as `cancelled`. - -#### Testing & Troubleshooting - -**Insert a test job (SQL)** -```sql -INSERT INTO Jobs (command, status, createdAt, updatedAt) -VALUES ('echo "Hello" && sleep 5 && echo "World"', 'pending', NOW(), NOW()); -``` - -**Inspect job status** -```sql -SELECT id, status, updatedAt FROM Jobs ORDER BY id DESC LIMIT 10; -``` - -**View job output** -```sql -SELECT id, output, createdAt FROM JobStatuses WHERE jobId = 1 ORDER BY id ASC; -``` - -**Long-running test (5 minutes)** -1. Stop runner to keep job pending -```bash -sudo systemctl stop job-runner.service -``` -2. Insert job -```bash -psql -c "INSERT INTO \"Jobs\" (command, status, \"createdAt\", \"updatedAt\") VALUES ('for i in \$(seq 1 300); do echo \"line \$i\"; sleep 1; done', 'pending', NOW(), NOW()) RETURNING id;" -``` -3. Start runner and monitor -```bash -node job-runner.js -# In another terminal: -while sleep 15; do - psql -c "SELECT id, output FROM \"JobStatuses\" WHERE \"jobId\"= ORDER BY id ASC;" -done -``` -4. Check final status -```sql -SELECT id, status FROM Jobs WHERE id = ; -``` - -#### Deployment Checklist - -- [ ] Run migrations: `npm run db:migrate` -- [ ] Deploy `job-runner.js` to target host (e.g., `/opt/container-creator/`) -- [ ] Copy `systemd/job-runner.service` to `/etc/systemd/system/` -- [ ] Create `/etc/default/container-creator` with DB env vars -- [ ] Reload systemd: `sudo systemctl daemon-reload` -- [ ] Enable and start: `sudo systemctl enable --now job-runner.service` -- [ ] Verify runner is running: `sudo systemctl status job-runner.service` -- [ ] Test API by creating a job via `POST /jobs` (admin user) - -#### Future Enhancements - -- Replace raw `command` API with safe task names and parameter mapping -- Add SSE or WebSocket streaming endpoint (`/jobs/:id/stream`) to push log lines to frontend in real-time -- Add batching or file-based logs for high-volume output to reduce DB pressure -- Implement job timeout/deadline and automatic cancellation - -### Configuration Routes - -#### `GET /sites/:siteId/nginx` -Generate nginx configuration for all registered services -- **Returns**: `text/plain` - Complete nginx configuration with all server blocks - -### User Registration Routes - -#### `GET /register` -Display account request form - -#### `POST /register` -Submit account request (sends email to admins) -- **Body**: `{ name, email, username, reason }` -- **Returns**: Success message - -### Utility Routes - -#### `GET /send-test-email` (Dev Only) -Test email configuration (development/testing) - -## Database Schema - -### Containers Table -```sql -id INT PRIMARY KEY AUTO_INCREMENT -hostname VARCHAR(255) UNIQUE NOT NULL -username VARCHAR(255) NOT NULL -status VARCHAR(20) NOT NULL DEFAULT 'pending' -template VARCHAR(255) -creationJobId INT FOREIGN KEY REFERENCES Jobs(id) -nodeId INT FOREIGN KEY REFERENCES Nodes(id) -containerId INT UNSIGNED NOT NULL -macAddress VARCHAR(17) UNIQUE -ipv4Address VARCHAR(45) UNIQUE -aiContainer VARCHAR(50) DEFAULT 'N' -createdAt DATETIME -updatedAt DATETIME -``` - -### Services Table -```sql -id INT PRIMARY KEY AUTO_INCREMENT -containerId INT FOREIGN KEY REFERENCES Containers(id) -type ENUM('tcp', 'udp', 'http') NOT NULL -internalPort INT NOT NULL -externalPort INT -tls BOOLEAN DEFAULT FALSE -externalHostname VARCHAR(255) -createdAt DATETIME -updatedAt DATETIME -``` - -## Configuration Files - -### `config/config.js` -Sequelize database configuration (reads from `.env`) - -### `models/` -- `container.js` - Container model definition -- `service.js` - Service model definition -- `index.js` - Sequelize initialization - -### `data/services.json` -Service type definitions and port mappings - -### `views/` -- `login.html` - Login form -- `form.html` - Container creation form -- `request-account.html` - Account request form -- `status.html` - Container creation progress viewer -- `containers.ejs` - Container list (EJS template) -- `nginx-conf.ejs` - Nginx config generator (EJS template) - -### `public/` -- `style.css` - Application styles - -### `migrations/` -Database migration files for schema management - -## Environment Variables - -### Required -- `POSTGRES_HOST` - Database host (default: localhost) -- `POSTGRES_USER` - Database username -- `POSTGRES_PASSWORD` - Database password -- `POSTGRES_DATABASE` - Database name -- `SESSION_SECRET` - Express session secret (cryptographically random string) - -### Optional -- `NODE_ENV` - Environment (development/production, default: development) - -## Security - -### Authentication -- Proxmox VE integration via API -- Session-based authentication with secure cookies -- Per-route authentication middleware - -### Rate Limiting -- 100 requests per 15-minute window per IP -- Protects against brute force and abuse - -### Session Security -- Session secret required for cookie signing -- Secure cookie flag enabled -- Session data server-side only - -### Input Validation -- URL encoding for all parameters -- Sequelize ORM prevents SQL injection -- Form data validation - -## Troubleshooting - -### Database Connection Issues -```bash -# Test database connection -psql -h localhost -U cluster_manager -d cluster_manager - -# Check if migrations ran -npm run db:migrate - -# Verify tables exist -psql -h localhost -U cluster_manager -d cluster_manager -c "\dt" -``` - -### Application Won't Start -```bash -# Check Node.js version -node --version # Should be 18.x or higher - -# Verify .env file exists and is readable -cat .env - -# Check for syntax errors -node -c server.js - -# Run with verbose logging -NODE_ENV=development node server.js -``` - -### Authentication Failing -```bash -# Verify Proxmox API is accessible -curl -k https://10.15.0.4:8006/api2/json/version - -# Check if certificate validation is working -# Edit server.js if using self-signed certs -``` - -### Email Not Sending -```bash -# Test SMTP connection -telnet mail.example.com 25 - -# Test route (development only) -curl http://localhost:3000/send-test-email -``` - -### Port Already in Use -```bash -# Find process using port 3000 -sudo lsof -i :3000 - -# Change port in .env or kill conflicting process -kill -9 -``` - -## Development - -### Database Migrations -```bash -# Create new migration +# Create a new migration npx sequelize-cli migration:generate --name description-here -# Run migrations +# Apply migrations + seeders manually npm run db:migrate -# Undo last migration +# Undo the last migration npx sequelize-cli db:migrate:undo ``` -### Code Structure -``` -create-a-container/ -├── server.js # Main Express application -├── package.json # Dependencies and scripts -├── .env # Environment configuration (gitignored) -├── config/ # Sequelize configuration -├── models/ # Database models -├── migrations/ # Database migrations -├── views/ # HTML templates -├── public/ # Static assets -├── data/ # JSON data files -└── bin/ # Utility scripts -``` - -## Integration with Nginx Reverse Proxy - -This application generates nginx configurations consumed by the `nginx-reverse-proxy` component: +The `seeders/` directory is for **development/test data only** (e.g. the local +`make dev` site and dummy node). Data that must exist on every deployment lives in +`migrations/`, so it is applied automatically at startup. -1. Containers register their services in the database -2. The `/sites/:siteId/nginx` endpoint generates complete nginx configs -3. The reverse proxy polls this endpoint via cron -4. Nginx automatically reloads with updated configurations +## API -See `../nginx-reverse-proxy/README.md` for reverse proxy setup. +The REST API is versioned under `/api/v1` and documented by the OpenAPI spec in +[`openapi.v1.yaml`](openapi.v1.yaml), browsable via the built-in Swagger UI at +`/api` when the server is running. -## License +## Layout -See the main repository LICENSE file. - -## Support - -For issues, questions, or contributions, see the main opensource-server repository. +``` +create-a-container/ +├── server.js # HTTP API + UI (Express) +├── job-runner.js # Background job worker +├── client/ # React/Vite single-page app (built to client/dist) +├── config/ # Sequelize configuration +├── models/ # Sequelize models +├── migrations/ # Database migrations (applied at startup) +├── seeders/ # Dev/test data seeders +├── routers/ # API and template routers +├── middlewares/ # Express middleware +├── utils/ # Shared helpers (incl. the migration runner) +├── views/ # EJS templates (nginx/dnsmasq config generation) +├── contrib/ # systemd units, logrotate, packaging hooks +└── Makefile # dev, build, and packaging targets +``` + +## License & support + +See the main repository [LICENSE](../LICENSE). For issues, questions, or +contributions, see the [opensource-server](https://github.com/mieweb/opensource-server) +repository. diff --git a/create-a-container/bin/dev-bootstrap.js b/create-a-container/bin/dev-bootstrap.js deleted file mode 100644 index f440322d..00000000 --- a/create-a-container/bin/dev-bootstrap.js +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env node -/** - * dev-bootstrap.js - * - * Provisions the minimum data needed to use the Manager locally WITHOUT a real - * Proxmox cluster. Intended to be run by `make dev` (and safe to run by hand). - * - * It creates, idempotently: - * - a `localhost` Site, - * - a `dummy` Node (nodeType: 'dummy') that acts as a mock hypervisor, and - * - a `localhost` ExternalDomain (so image templates that expose an HTTP port, - * which auto-add an HTTP service, have a domain to bind to — otherwise the - * new-container form is unsubmittable). - * - * IMPORTANT: This is deliberately NOT wired into the `/auth/dev` login route. - * Doing so previously clobbered the Docker Compose bootstrap, which calls - * `POST /auth/dev` before creating its own "Development" site — stealing site - * id 1 and importing real nodes/containers into the wrong site. - * - * To stay clear of that path entirely, this script is a NO-OP if ANY Site - * already exists. The Docker stack always creates its own site, so running this - * there does nothing. It only ever populates a fresh, empty local database. - * - * Refuses to run when NODE_ENV === 'production'. - * - * Exit code 0 = success (including the intentional no-op). - */ - -const path = require('path'); -const db = require(path.join(__dirname, '..', 'models')); -const { sequelize, Site, Node, ExternalDomain } = db; - -const DUMMY_NODE_NAME = 'local-dummy'; - -async function main() { - if (process.env.NODE_ENV === 'production') { - console.error('dev-bootstrap: refusing to run in production'); - process.exit(1); - } - - await sequelize.authenticate(); - - // No-op if any site exists — never interfere with an existing environment - // (e.g. the Docker Compose "Development" site). - const existingSiteCount = await Site.count(); - if (existingSiteCount > 0) { - console.log( - `dev-bootstrap: ${existingSiteCount} site(s) already present — nothing to do.`, - ); - return; - } - - console.log('dev-bootstrap: empty database detected, provisioning local dev environment...'); - - const site = await Site.create({ - name: 'localhost', - internalDomain: 'localhost', - dhcpRange: '10.0.0.100,10.0.0.250', - subnetMask: '255.255.255.0', - gateway: '10.0.0.1', - dnsForwarders: '1.1.1.1,8.8.8.8', - externalIp: '127.0.0.1', - }); - console.log(` • Site "localhost" created (id=${site.id})`); - - const node = await Node.create({ - name: DUMMY_NODE_NAME, - nodeType: 'dummy', - siteId: site.id, - ipv4Address: '127.0.0.1', - // Placeholder credentials: a dummy node never talks to a real hypervisor, - // but these must be non-null so code that gates on "has API credentials" - // (e.g. the live container-status resolver) routes through node.api() and - // gets the DummyApi instead of treating the node as unreachable. - apiUrl: 'local', - tokenId: 'local', - secret: 'local', - nvidiaAvailable: false, - }); - console.log(` • Dummy Node "${node.name}" created (id=${node.id}, nodeType=dummy)`); - - const domain = await ExternalDomain.create({ - name: 'localhost', - siteId: site.id, - }); - console.log(` • ExternalDomain "localhost" created (id=${domain.id})`); - - console.log('dev-bootstrap: done. The Manager can now create containers locally (simulated).'); -} - -main() - .then(() => process.exit(0)) - .catch((err) => { - console.error('dev-bootstrap failed:', err.message); - process.exit(1); - }); diff --git a/create-a-container/job-runner.js b/create-a-container/job-runner.js index 644b7c97..83a4b850 100644 --- a/create-a-container/job-runner.js +++ b/create-a-container/job-runner.js @@ -10,6 +10,7 @@ const { spawn } = require('child_process'); const path = require('path'); const db = require('./models'); +const { runMigrations } = require('./utils/migrate'); const POLL_INTERVAL_MS = parseInt(process.env.JOB_RUNNER_POLL_MS || '2000', 10); const WORKDIR = process.env.JOB_RUNNER_CWD || process.cwd(); @@ -156,6 +157,7 @@ process.on('SIGTERM', () => { shutdownAndCancelJobs('SIGTERM').catch(err => { co async function start() { console.log('JobRunner starting, working dir:', WORKDIR); + await runMigrations(db.sequelize); await db.sequelize.authenticate(); console.log('DB connected'); loop(); diff --git a/create-a-container/seeders/20251121000000-seed-groups.js b/create-a-container/migrations/20260616000000-seed-groups.js similarity index 78% rename from create-a-container/seeders/20251121000000-seed-groups.js rename to create-a-container/migrations/20260616000000-seed-groups.js index ad4978cc..c2815b7a 100644 --- a/create-a-container/seeders/20251121000000-seed-groups.js +++ b/create-a-container/migrations/20260616000000-seed-groups.js @@ -1,19 +1,18 @@ 'use strict'; +// Seeds the two built-in groups (sysadmins, ldapusers). const GID_MIN = 2000; /** @type {import('sequelize-cli').Migration} */ module.exports = { - async up(queryInterface, Sequelize) { + async up(queryInterface) { const now = new Date(); const groups = [ { gidNumber: GID_MIN, cn: 'sysadmins', isAdmin: true, createdAt: now, updatedAt: now }, { gidNumber: GID_MIN + 1, cn: 'ldapusers', isAdmin: false, createdAt: now, updatedAt: now }, ]; - // Idempotent: only insert groups that don't already exist, so re-running - // seeders (e.g. `make dev`) doesn't violate the unique constraints on - // gidNumber/cn. + // Idempotent: only insert groups that don't already exist. const [existing] = await queryInterface.sequelize.query( `SELECT "gidNumber" FROM "Groups" WHERE "gidNumber" IN (:gids)`, { replacements: { gids: groups.map((g) => g.gidNumber) } }, @@ -25,7 +24,7 @@ module.exports = { await queryInterface.bulkInsert('Groups', toInsert, {}); }, - async down(queryInterface, Sequelize) { + async down(queryInterface) { await queryInterface.bulkDelete('Groups', { gidNumber: [GID_MIN, GID_MIN + 1] }, {}); diff --git a/create-a-container/seeders/20260311000000-seed-wazuh-env-vars.js b/create-a-container/migrations/20260616000001-seed-wazuh-env-vars.js similarity index 93% rename from create-a-container/seeders/20260311000000-seed-wazuh-env-vars.js rename to create-a-container/migrations/20260616000001-seed-wazuh-env-vars.js index 7ef8fc13..0969f2ec 100644 --- a/create-a-container/seeders/20260311000000-seed-wazuh-env-vars.js +++ b/create-a-container/migrations/20260616000001-seed-wazuh-env-vars.js @@ -1,8 +1,7 @@ 'use strict'; -// Variables seeded into the default_container_env_vars setting. -// Add future cross-container variables here and create a new seeder -// that calls the same merge logic. +// Seeds the Wazuh agent enrollment variables into the +// default_container_env_vars setting. const WAZUH_DEFAULTS = [ { key: 'WAZUH_MANAGER', diff --git a/create-a-container/seeders/20260604000000-seed-sssd-env-vars.js b/create-a-container/migrations/20260616000002-seed-sssd-env-vars.js similarity index 92% rename from create-a-container/seeders/20260604000000-seed-sssd-env-vars.js rename to create-a-container/migrations/20260616000002-seed-sssd-env-vars.js index b317aea6..fb912933 100644 --- a/create-a-container/seeders/20260604000000-seed-sssd-env-vars.js +++ b/create-a-container/migrations/20260616000002-seed-sssd-env-vars.js @@ -1,9 +1,9 @@ 'use strict'; -// Variables seeded into the default_container_env_vars setting for the -// base/sssd.conf.template. Only SSSD_LDAP_URI and SSSD_LDAP_TLS_REQCERT -// carry default values; the remaining variables are intentionally left -// blank so that sssd falls back to its builtin defaults. +// Seeds the SSSD env vars for the base/sssd.conf.template into the +// default_container_env_vars setting. Only SSSD_LDAP_URI and +// SSSD_LDAP_TLS_REQCERT carry default values; the remaining variables are +// intentionally left blank so that sssd falls back to its builtin defaults. const SSSD_DEFAULTS = [ { key: 'SSSD_LDAP_URI', diff --git a/create-a-container/seeders/20260604000003-seed-sssd-access-filter-and-user-attrs.js b/create-a-container/migrations/20260616000003-seed-sssd-access-filter-and-user-attrs.js similarity index 94% rename from create-a-container/seeders/20260604000003-seed-sssd-access-filter-and-user-attrs.js rename to create-a-container/migrations/20260616000003-seed-sssd-access-filter-and-user-attrs.js index af443093..f828f685 100644 --- a/create-a-container/seeders/20260604000003-seed-sssd-access-filter-and-user-attrs.js +++ b/create-a-container/migrations/20260616000003-seed-sssd-access-filter-and-user-attrs.js @@ -5,10 +5,6 @@ // - SSSD_LDAP_USER_GECOS (full-name/gecos attribute; defaults to cn) // - SSSD_LDAP_ACCESS_FILTER (login access filter) // -// This is a separate seeder (rather than an edit to 20260604000000) because -// that seeder is already released and recorded as executed in existing -// databases, so editing it in place would not back-fill the new keys. -// // SSSD_LDAP_ACCESS_FILTER defaults to a permissive filter that every directory // entry matches, so out of the box all directory-authenticated users may log // in. This is deliberate: with access_provider=ldap and diff --git a/create-a-container/package-lock.json b/create-a-container/package-lock.json index 0f87e7bf..ca9a1c69 100644 --- a/create-a-container/package-lock.json +++ b/create-a-container/package-lock.json @@ -22,14 +22,15 @@ "patch-package": "^8.0.1", "pg": "^8.16.3", "sequelize": "^6.37.8", - "sequelize-cli": "^6.6.3", "swagger-ui-express": "^5.0.1", + "umzug": "^2.3.0", "yamljs": "^0.3.0" }, "devDependencies": { "@vscode/sqlite3": "5.1.12-vscode", "concurrently": "^9.2.3", - "nodemon": "^3.1.10" + "nodemon": "^3.1.10", + "sequelize-cli": "^6.6.3" } }, "node_modules/@epic-web/invariant": { @@ -41,6 +42,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -71,6 +73,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, "license": "MIT" }, "node_modules/@phc/format": { @@ -85,6 +88,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -150,6 +154,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -171,6 +176,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -183,6 +189,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -245,6 +252,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, "license": "ISC", "engines": { "node": ">= 4.0.0" @@ -523,6 +531,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -534,6 +543,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -543,6 +553,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -558,12 +569,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -578,6 +591,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -590,6 +604,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -637,6 +652,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -826,6 +842,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, "license": "MIT", "dependencies": { "ini": "^1.3.4", @@ -1009,12 +1026,14 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/editorconfig": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, "license": "MIT", "dependencies": { "@one-ini/wasm": "0.1.1", @@ -1033,6 +1052,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -1042,6 +1062,7 @@ "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" @@ -1077,6 +1098,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -1133,6 +1155,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1349,6 +1372,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -1418,6 +1442,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", @@ -1462,6 +1487,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -1506,6 +1532,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -1538,6 +1565,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -1547,6 +1575,7 @@ "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" @@ -1690,6 +1719,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, "license": "ISC" }, "node_modules/ip-address": { @@ -1726,6 +1756,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -1766,6 +1797,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1826,6 +1858,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -1867,6 +1900,7 @@ "version": "1.15.4", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, "license": "MIT", "dependencies": { "config-chain": "^1.1.13", @@ -1888,6 +1922,7 @@ "version": "3.0.8", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify": { @@ -2047,6 +2082,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -2203,6 +2239,7 @@ "version": "7.2.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, "license": "ISC", "dependencies": { "abbrev": "^2.0.0" @@ -2324,6 +2361,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parseurl": { @@ -2399,12 +2437,14 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -2421,6 +2461,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, "license": "ISC" }, "node_modules/path-to-regexp": { @@ -2583,6 +2624,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, "license": "ISC" }, "node_modules/proxy-addr": { @@ -2690,6 +2732,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2699,6 +2742,7 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -2869,6 +2913,7 @@ "version": "6.6.3", "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.3.tgz", "integrity": "sha512-1YYPrcSRt/bpMDDSKM5ubY1mnJ2TEwIaGZcqITw4hLtGtE64nIqaBnLtMvH8VKHg6FbWpXTiFNc2mS/BtQCXZw==", + "dev": true, "license": "MIT", "dependencies": { "fs-extra": "^9.1.0", @@ -3038,6 +3083,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -3095,6 +3141,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -3113,6 +3160,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3127,6 +3175,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3136,12 +3185,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3154,6 +3205,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -3170,6 +3222,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3182,6 +3235,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3204,6 +3258,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3434,6 +3489,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -3452,6 +3508,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -3469,6 +3526,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3478,6 +3536,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3493,12 +3552,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3513,6 +3574,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3539,6 +3601,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -3608,6 +3671,7 @@ "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^7.0.2", @@ -3626,6 +3690,7 @@ "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -3635,6 +3700,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3644,12 +3710,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3664,6 +3732,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" diff --git a/create-a-container/package.json b/create-a-container/package.json index 8f7b4139..d69379e9 100644 --- a/create-a-container/package.json +++ b/create-a-container/package.json @@ -35,13 +35,14 @@ "patch-package": "^8.0.1", "pg": "^8.16.3", "sequelize": "^6.37.8", - "sequelize-cli": "^6.6.3", "swagger-ui-express": "^5.0.1", + "umzug": "^2.3.0", "yamljs": "^0.3.0" }, "devDependencies": { "@vscode/sqlite3": "5.1.12-vscode", "concurrently": "^9.2.3", - "nodemon": "^3.1.10" + "nodemon": "^3.1.10", + "sequelize-cli": "^6.6.3" } } diff --git a/create-a-container/seeders/.gitkeep b/create-a-container/seeders/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/create-a-container/seeders/20260120165612-push-notification-settings.js b/create-a-container/seeders/20260120165612-push-notification-settings.js deleted file mode 100644 index b797f94c..00000000 --- a/create-a-container/seeders/20260120165612-push-notification-settings.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - async up(queryInterface, Sequelize) { - const now = new Date(); - const settings = [ - { key: 'push_notification_url', value: '', createdAt: now, updatedAt: now }, - { key: 'push_notification_enabled', value: 'false', createdAt: now, updatedAt: now }, - ]; - - // Idempotent: only insert keys that don't already exist, so re-running - // seeders doesn't violate the unique constraint on Settings.key. - const [existing] = await queryInterface.sequelize.query( - `SELECT "key" FROM "Settings" WHERE "key" IN (:keys)`, - { replacements: { keys: settings.map((s) => s.key) } }, - ); - const existingKeys = new Set(existing.map((r) => r.key)); - const toInsert = settings.filter((s) => !existingKeys.has(s.key)); - if (toInsert.length === 0) return; - - await queryInterface.bulkInsert('Settings', toInsert, {}); - }, - - async down(queryInterface, Sequelize) { - await queryInterface.bulkDelete('Settings', { - key: ['push_notification_url', 'push_notification_enabled'] - }, {}); - } -}; diff --git a/create-a-container/seeders/20260313000000-push-notification-api-key.js b/create-a-container/seeders/20260313000000-push-notification-api-key.js deleted file mode 100644 index ff50e358..00000000 --- a/create-a-container/seeders/20260313000000-push-notification-api-key.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - async up(queryInterface, Sequelize) { - // Idempotent: skip if the key already exists, so re-running seeders doesn't - // violate the unique constraint on Settings.key. - const [existing] = await queryInterface.sequelize.query( - `SELECT "key" FROM "Settings" WHERE "key" = 'push_notification_api_key'`, - ); - if (existing.length > 0) return; - - const now = new Date(); - await queryInterface.bulkInsert('Settings', [ - { key: 'push_notification_api_key', value: '', createdAt: now, updatedAt: now }, - ], {}); - }, - - async down(queryInterface, Sequelize) { - await queryInterface.bulkDelete('Settings', { - key: 'push_notification_api_key' - }, {}); - } -}; diff --git a/create-a-container/seeders/20260616000000-seed-dev-environment.js b/create-a-container/seeders/20260616000000-seed-dev-environment.js new file mode 100644 index 00000000..b1aaa947 --- /dev/null +++ b/create-a-container/seeders/20260616000000-seed-dev-environment.js @@ -0,0 +1,121 @@ +'use strict'; + +/** + * Local development environment seeder. + * + * Provisions the minimum data needed to use the Manager locally WITHOUT a real + * Proxmox cluster. Run automatically by `make dev` (via `npm run db:migrate`, + * which calls `sequelize-cli db:seed:all`) and safe to run by hand with + * `npx sequelize-cli db:seed:all`. + * + * It creates, idempotently: + * - a `localhost` Site, + * - a `local-dummy` Node (nodeType: 'dummy') that acts as a mock hypervisor, + * and + * - a `localhost` ExternalDomain (so image templates that expose an HTTP port, + * which auto-add an HTTP service, have a domain to bind to — otherwise the + * new-container form is unsubmittable). + * + * IMPORTANT: This is deliberately NOT wired into the `/auth/dev` login route. + * Doing so previously clobbered the Docker Compose bootstrap, which calls + * `POST /auth/dev` before creating its own "Development" site — stealing site + * id 1 and importing real nodes/containers into the wrong site. + * + * To stay clear of that path entirely, this seeder is a NO-OP if ANY Site + * already exists. The Docker stack always creates its own site, so running this + * there does nothing. It only ever populates a fresh, empty local database. + * + * It is also a NO-OP when NODE_ENV === 'production' — this is development-only + * data and must never seed a production database. + */ + +const DUMMY_NODE_NAME = 'local-dummy'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface) { + if (process.env.NODE_ENV === 'production') { + console.log('seed dev-environment: NODE_ENV=production — skipping (dev-only data).'); + return; + } + + // Use the dialect-aware query generator to quote identifiers/literals so the + // raw lookups below work regardless of DATABASE_DIALECT (Postgres uses + // double quotes, MySQL/MariaDB use backticks, etc.). + const qg = queryInterface.queryGenerator; + const sitesTable = qg.quoteTable('Sites'); + + // No-op if any site exists — never interfere with an existing environment + // (e.g. the Docker Compose "Development" site). + const [siteRows] = await queryInterface.sequelize.query( + `SELECT COUNT(*) AS count FROM ${sitesTable}`, + ); + const existingSiteCount = Number(siteRows[0].count); + if (existingSiteCount > 0) { + console.log( + `seed dev-environment: ${existingSiteCount} site(s) already present — nothing to do.`, + ); + return; + } + + console.log('seed dev-environment: empty database detected, provisioning local dev environment...'); + + const now = new Date(); + + await queryInterface.bulkInsert('Sites', [{ + name: 'localhost', + internalDomain: 'localhost', + dhcpRange: '10.0.0.100,10.0.0.250', + subnetMask: '255.255.255.0', + gateway: '10.0.0.1', + dnsForwarders: '1.1.1.1,8.8.8.8', + externalIp: '127.0.0.1', + createdAt: now, + updatedAt: now, + }]); + + // Fetch the id of the site just created so the node/domain can reference it. + const [createdSiteRows] = await queryInterface.sequelize.query( + `SELECT ${qg.quoteIdentifier('id')} FROM ${sitesTable} ` + + `WHERE ${qg.quoteIdentifier('name')} = ${qg.escape('localhost')} ` + + `ORDER BY ${qg.quoteIdentifier('id')} ASC LIMIT 1`, + ); + const siteId = createdSiteRows[0].id; + console.log(` • Site "localhost" created (id=${siteId})`); + + await queryInterface.bulkInsert('Nodes', [{ + name: DUMMY_NODE_NAME, + nodeType: 'dummy', + siteId, + ipv4Address: '127.0.0.1', + // Placeholder credentials: a dummy node never talks to a real hypervisor, + // but these must be non-null so code that gates on "has API credentials" + // (e.g. the live container-status resolver) routes through node.api() and + // gets the DummyApi instead of treating the node as unreachable. + apiUrl: 'local', + tokenId: 'local', + secret: 'local', + nvidiaAvailable: false, + createdAt: now, + updatedAt: now, + }]); + console.log(` • Dummy Node "${DUMMY_NODE_NAME}" created (nodeType=dummy)`); + + await queryInterface.bulkInsert('ExternalDomains', [{ + name: 'localhost', + siteId, + createdAt: now, + updatedAt: now, + }]); + console.log(' • ExternalDomain "localhost" created'); + + console.log('seed dev-environment: done. The Manager can now create containers locally (simulated).'); + }, + + async down(queryInterface) { + // Remove only the specific local-dev records this seeder creates. + await queryInterface.bulkDelete('Nodes', { name: DUMMY_NODE_NAME }, {}); + await queryInterface.bulkDelete('ExternalDomains', { name: 'localhost' }, {}); + await queryInterface.bulkDelete('Sites', { name: 'localhost' }, {}); + } +}; diff --git a/create-a-container/server.js b/create-a-container/server.js index 650384b3..6ef567e6 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -12,6 +12,7 @@ const net = require('net'); const swaggerUi = require('swagger-ui-express'); const YAML = require('yamljs'); const { sequelize, SessionSecret } = require('./models'); +const { runMigrations } = require('./utils/migrate'); // Function to get or create session secrets @@ -33,6 +34,9 @@ async function getSessionSecrets() { } async function main() { + // Apply any pending database migrations before serving traffic + await runMigrations(sequelize); + const app = express(); // setup views (still used by templates router for nginx-conf / dnsmasq files) @@ -132,4 +136,7 @@ async function main() { app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); } -main(); +main().catch(err => { + console.error('Fatal: server failed to start:', err); + process.exit(1); +}); diff --git a/create-a-container/utils/migrate.js b/create-a-container/utils/migrate.js new file mode 100644 index 00000000..05c88286 --- /dev/null +++ b/create-a-container/utils/migrate.js @@ -0,0 +1,192 @@ +'use strict'; + +/** + * utils/migrate.js + * + * Programmatic database migration runner executed at process startup. + * + * Migrations are applied with Umzug (the same library, and the same v2 API, + * that sequelize-cli uses internally) against the `SequelizeMeta` table, so the + * set of already-applied migrations is shared byte-for-byte with the + * `sequelize db:migrate` CLI command. Migrations that have already run are + * skipped. + * + * To guarantee that only one process applies migrations at a time (e.g. when a + * server is restarting while another instance boots, or alongside any legacy + * init tooling), the run is wrapped in an engine-appropriate ADVISORY LOCK: + * + * - PostgreSQL: pg_advisory_lock / pg_advisory_unlock (session-scoped). The + * lock is taken and released on a single dedicated connection that is held + * open for the entire migration batch. + * - MySQL/MariaDB: GET_LOCK / RELEASE_LOCK (session-scoped), likewise on a + * single dedicated connection. + * - SQLite: no advisory-lock primitive exists and concurrent processes + * sharing one SQLite file is not a supported production topology, so + * locking is skipped (SQLite already serializes writers via its file lock). + * + * On failure the error is propagated so the caller can exit non-zero and let + * the service manager (systemd Restart=on-failure) retry. + */ + +const path = require('path'); +const Umzug = require('umzug'); + +// A stable, application-specific identifier for the migration advisory lock. +// Postgres advisory locks are keyed by a signed 64-bit integer; we derive two +// 32-bit "classid"/"objid" halves from a hash of this string and use the +// two-argument form pg_advisory_lock(int4, int4) so the constant is readable +// and collision-resistant against other advisory-lock users. +const LOCK_NAMESPACE = 'create-a-container:migrations'; + +/** + * Deterministically derive a pair of signed 32-bit integers from a string, + * suitable for pg_advisory_lock(classid int4, objid int4). Uses a simple + * FNV-1a-style hash; the exact algorithm does not matter as long as it is + * stable across processes and versions. + * + * @param {string} str + * @returns {{ classId: number, objId: number }} + */ +function deriveLockKey(str) { + // Two independent 32-bit hashes (different offsets) for the two halves. + const hash32 = (seed) => { + let h = seed >>> 0; + for (let i = 0; i < str.length; i++) { + h ^= str.charCodeAt(i); + h = Math.imul(h, 0x01000193); // FNV prime + } + // Convert the unsigned 32-bit result to a signed int4 for Postgres. + return h | 0; + }; + return { classId: hash32(0x811c9dc5), objId: hash32(0x7ee3623b) }; +} + +/** + * Build the Umzug v2 instance bound to the project's migrations directory and + * the shared Sequelize connection. Each migration is invoked as + * `up(queryInterface, Sequelize)` / `down(queryInterface, Sequelize)`, matching + * sequelize-cli's calling convention. + * + * @param {import('sequelize').Sequelize} sequelize + * @returns {Umzug.Umzug} + */ +function buildUmzug(sequelize) { + return new Umzug({ + storage: 'sequelize', + storageOptions: { sequelize }, + logging: (...args) => console.log('[migrate]', ...args), + migrations: { + path: path.join(__dirname, '..', 'migrations'), + // Pass the same params sequelize-cli passes to migration up()/down(). + // `sequelize.constructor` is the Sequelize class (carries DataTypes etc.). + params: [sequelize.getQueryInterface(), sequelize.constructor], + pattern: /^\d.+\.js$/, + }, + }); +} + +/** + * Acquire the advisory lock for the given dialect on a dedicated connection, + * returning an async release function. For SQLite (or any dialect without an + * advisory-lock primitive) this is a no-op. + * + * The returned release function never throws; it logs and swallows errors so a + * failure to unlock cannot mask the real migration outcome. + * + * @param {import('sequelize').Sequelize} sequelize + * @returns {Promise<() => Promise>} + */ +async function acquireAdvisoryLock(sequelize) { + const dialect = sequelize.getDialect(); + + if (dialect === 'postgres') { + const { classId, objId } = deriveLockKey(LOCK_NAMESPACE); + // Hold a single connection for the lifetime of the lock; session-level + // advisory locks belong to the connection that acquired them. + const connection = await sequelize.connectionManager.getConnection(); + console.log(`[migrate] acquiring postgres advisory lock (${classId}, ${objId})...`); + try { + await connection.query('SELECT pg_advisory_lock($1, $2)', [classId, objId]); + } catch (err) { + // Acquisition failed (permission/network error, etc.) — return the + // connection to the pool so it isn't leaked across startup retries. + sequelize.connectionManager.releaseConnection(connection); + throw err; + } + console.log('[migrate] postgres advisory lock acquired'); + return async () => { + try { + await connection.query('SELECT pg_advisory_unlock($1, $2)', [classId, objId]); + console.log('[migrate] postgres advisory lock released'); + } catch (err) { + console.error('[migrate] failed to release postgres advisory lock:', err); + } finally { + sequelize.connectionManager.releaseConnection(connection); + } + }; + } + + if (dialect === 'mysql' || dialect === 'mariadb') { + // MySQL GET_LOCK is keyed by a string name; -1 = wait indefinitely. + const connection = await sequelize.connectionManager.getConnection(); + console.log(`[migrate] acquiring mysql advisory lock "${LOCK_NAMESPACE}"...`); + let acquired = false; + try { + const [rows] = await connection.query('SELECT GET_LOCK(?, ?) AS acquired', [LOCK_NAMESPACE, -1]); + acquired = rows && rows[0] && Number(rows[0].acquired) === 1; + } catch (err) { + // Query failed (e.g. connection dropped) before returning — release the + // connection so it isn't leaked across startup retries. + sequelize.connectionManager.releaseConnection(connection); + throw err; + } + if (!acquired) { + sequelize.connectionManager.releaseConnection(connection); + throw new Error(`Could not acquire MySQL advisory lock "${LOCK_NAMESPACE}" for migrations`); + } + console.log('[migrate] mysql advisory lock acquired'); + return async () => { + try { + await connection.query('SELECT RELEASE_LOCK(?)', [LOCK_NAMESPACE]); + console.log('[migrate] mysql advisory lock released'); + } catch (err) { + console.error('[migrate] failed to release mysql advisory lock:', err); + } finally { + sequelize.connectionManager.releaseConnection(connection); + } + }; + } + + // sqlite (default) and anything else: no advisory lock available/needed. + console.log(`[migrate] dialect "${dialect}" has no advisory lock; relying on engine-level serialization`); + return async () => {}; +} + +/** + * Run all pending database migrations, serialized by an engine-appropriate + * advisory lock. Resolves once migrations are up to date; rejects (without + * starting the server) if any migration fails. + * + * @param {import('sequelize').Sequelize} sequelize - the shared connection from ../models + * @returns {Promise} names of migrations that were applied this run + */ +async function runMigrations(sequelize) { + const release = await acquireAdvisoryLock(sequelize); + try { + const umzug = buildUmzug(sequelize); + const pending = await umzug.pending(); + if (pending.length === 0) { + console.log('[migrate] database schema is up to date; no migrations to run'); + return []; + } + console.log(`[migrate] applying ${pending.length} pending migration(s)...`); + const applied = await umzug.up(); + const names = applied.map((m) => m.file); + console.log(`[migrate] applied ${names.length} migration(s): ${names.join(', ')}`); + return names; + } finally { + await release(); + } +} + +module.exports = { runMigrations }; diff --git a/images/manager/container-creator-init.service b/images/manager/container-creator-init.service index 8c9feac2..e898e2ee 100644 --- a/images/manager/container-creator-init.service +++ b/images/manager/container-creator-init.service @@ -22,9 +22,7 @@ ExecStart=/bin/bash -e -c '\ echo "POSTGRES_HOST=$${POSTGRES_HOST}" >> /etc/default/container-creator; \ echo "POSTGRES_DATABASE=$${POSTGRES_DATABASE}" >> /etc/default/container-creator; \ echo "POSTGRES_USER=$${POSTGRES_USER}" >> /etc/default/container-creator; \ - echo "POSTGRES_PASSWORD=$${POSTGRES_PASSWORD}" >> /etc/default/container-creator; \ - node_modules/.bin/sequelize db:migrate; \ - node_modules/.bin/sequelize db:seed:all;' + echo "POSTGRES_PASSWORD=$${POSTGRES_PASSWORD}" >> /etc/default/container-creator;' [Install] WantedBy=multi-user.target