Skip to content
Merged
17 changes: 16 additions & 1 deletion create-a-container/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ erDiagram
int id PK
string hostname UK "FQDN hostname"
string username "Owner username"
string status "pending,creating,running,failed"
string template "Template name"
int creationJobId FK "References Job"
int nodeId FK "References Node"
Expand Down Expand Up @@ -231,6 +230,22 @@ Delete a container from both Proxmox and the database
- `403` - User doesn't own the container
- `500` - Proxmox API deletion failed or node not configured

#### 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

Comment thread
runleveldev marked this conversation as resolved.
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

Expand Down
28 changes: 10 additions & 18 deletions create-a-container/bin/create-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,14 @@ async function main() {
console.error(`Container with ID ${containerId} not found`);
process.exit(1);
}

if (container.status !== 'pending') {
console.error(`Container is not in pending status (current: ${container.status})`);

// Guard against double-provisioning. The status column no longer exists, so
// we use the Proxmox VMID as the signal: once a VMID has been allocated and
// stored, creation has already run for this container.
if (container.containerId) {
console.error(
`Container already has a Proxmox VMID (${container.containerId}); refusing to re-create`,
);
process.exit(1);
}

Expand Down Expand Up @@ -217,10 +222,6 @@ async function main() {
console.log(`Template type: ${isDocker ? 'Docker image' : 'Proxmox template'}`);

try {
// Update status to 'creating'
await container.update({ status: 'creating' });
console.log('Status updated to: creating');

// Get the Proxmox API client
const client = await node.api();
console.log('Proxmox API client initialized');
Expand Down Expand Up @@ -428,8 +429,7 @@ async function main() {
console.log('Updating container record...');
await container.update({
macAddress,
ipv4Address,
status: 'running'
ipv4Address
});

console.log('Container creation completed successfully!');
Expand Down Expand Up @@ -480,15 +480,7 @@ async function main() {
if (err.response?.data) {
console.error('API Error Details:', JSON.stringify(err.response.data, null, 2));
}

// Update status to failed
try {
await container.update({ status: 'failed' });
console.log('Status updated to: failed');
} catch (updateErr) {
console.error('Failed to update container status:', updateErr.message);
}


process.exit(1);
}
}
Expand Down
2 changes: 0 additions & 2 deletions create-a-container/bin/json-to-sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ async function run() {
hostname,
ipv4Address: obj.ip,
username: obj.user || '',
status: 'running',
template: obj.template || null,
containerId: obj.ctid,
macAddress: obj.mac,
Expand All @@ -234,7 +233,6 @@ async function run() {
await container.update({
ipv4Address: obj.ip,
username: obj.user || '',
status: container.status || 'running',
template: obj.template || container.template,
containerId: obj.ctid,
macAddress: obj.mac
Expand Down
15 changes: 3 additions & 12 deletions create-a-container/bin/reconfigure-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,13 @@ async function main() {
throw new Error('Could not get IP address from Proxmox interfaces API');
}

// Update container record with MAC/IP and running status
// Update container record with MAC/IP
await container.update({
status: 'running',
macAddress,
ipv4Address
});

console.log('Status updated to: running');
console.log('Reconfiguration applied; container running');
console.log(` MAC: ${macAddress}`);
console.log(` IP: ${ipv4Address}`);
}
Expand All @@ -181,15 +180,7 @@ async function main() {
if (err.response?.data) {
console.error('API Error Details:', JSON.stringify(err.response.data, null, 2));
}

// Update status to failed
try {
await container.update({ status: 'failed' });
console.log('Status updated to: failed');
} catch (updateErr) {
console.error('Failed to update container status:', updateErr.message);
}


process.exit(1);
}
}
Expand Down
16 changes: 14 additions & 2 deletions create-a-container/client/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export interface Container {
hostname: string;
ipv4Address: string | null;
macAddress: string | null;
status: string;
status: ContainerStatus;
template: string | null;
creationJobId: number | null;
entrypoint: string | null;
Expand All @@ -90,11 +90,23 @@ export interface Container {
createdAt: string;
}

/**
* Live container status resolved from Proxmox run-state + create-job state.
* Embedded on each Container returned by the list/show/create endpoints.
*/
export type ContainerStatus =
| 'running'
| 'offline'
| 'creating'
| 'failed'
| 'missing'
| 'unknown';

export interface ContainerCreateResult {
containerId: number;
jobId: number;
hostname: string;
status: string;
status: ContainerStatus;
}

export interface ContainerNewBootstrap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,44 @@ import {
import { api, ApiError } from '@/lib/api';
import { useSession } from '@/lib/auth';
import { keys, queries } from '@/lib/queries';
import type { Container } from '@/lib/types';
import type { Container, ContainerStatus } from '@/lib/types';

type ViewMode = 'cards' | 'table';
const VIEW_STORAGE_KEY = 'containers:view';

function statusVariant(s: string): 'default' | 'success' | 'warning' | 'danger' | 'secondary' {
function statusVariant(
s: ContainerStatus,
): 'default' | 'success' | 'warning' | 'danger' | 'secondary' {
switch (s) {
case 'running':
return 'success';
case 'pending':
case 'restarting':
case 'creating':
return 'warning';
case 'failed':
case 'error':
return 'danger';
case 'stopped':
case 'offline':
case 'missing':
return 'secondary';
default:
return 'default';
}
}

// Human-readable labels for the live status values.
const STATUS_LABELS: Record<ContainerStatus, string> = {
running: 'Running',
offline: 'Offline',
creating: 'Creating',
failed: 'Failed',
missing: 'Missing',
unknown: 'Unknown',
};

/** Status badge. The status is the live value embedded in the list response. */
function StatusBadge({ status }: { status: ContainerStatus }) {
return <Badge variant={statusVariant(status)}>{STATUS_LABELS[status] ?? status}</Badge>;
}

const linkClass = 'text-(--color-primary,#1d4ed8) hover:underline';

/** Shorten a full image ref to just its name+tag, e.g. ghcr.io/mieweb/base:latest -> base:latest */
Expand Down Expand Up @@ -330,7 +346,7 @@ export function ContainersListPage() {
<CardTitle as="h2" className="truncate text-sm font-semibold">
{c.hostname}
</CardTitle>
<Badge variant={statusVariant(c.status)}>{c.status}</Badge>
<StatusBadge status={c.status} />
</div>
<div className="ml-auto flex shrink-0 items-center gap-1 lg:order-3 lg:ml-0">
<RowActions c={c} siteId={siteId} onDelete={del.mutate} deleting={del.isPending} />
Expand Down Expand Up @@ -374,7 +390,7 @@ export function ContainersListPage() {
<TableRow key={c.id}>
<TableCell className="font-medium">{c.hostname}</TableCell>
<TableCell>
<Badge variant={statusVariant(c.status)}>{c.status}</Badge>
<StatusBadge status={c.status} />
</TableCell>
<TableCell>
<NodeLink c={c} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */

/**
* Remove the static `status` column from Containers.
*
* Container status is now computed live from Proxmox + job state + config drift
* (see utils/container-status.js) and embedded on the container API payloads, so
* the persisted column is no longer a source of truth and is dropped.
*
* The down migration restores the column (defaulting to 'running') for rollback,
* but the historical per-container value cannot be recovered.
*/
module.exports = {
async up(queryInterface) {
await queryInterface.removeColumn('Containers', 'status');
},
Comment thread
runleveldev marked this conversation as resolved.

async down(queryInterface, Sequelize) {
await queryInterface.addColumn('Containers', 'status', {
type: Sequelize.STRING(20),
allowNull: false,
defaultValue: 'running',
});
},
};
5 changes: 0 additions & 5 deletions create-a-container/models/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,6 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.STRING(255),
allowNull: false
},
status: {
type: DataTypes.STRING(20),
allowNull: false,
defaultValue: 'pending'
},
template: {
type: DataTypes.STRING(255),
allowNull: true
Expand Down
20 changes: 19 additions & 1 deletion create-a-container/openapi.v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ components:
id: { type: integer }
hostname: { type: string }
containerId: { type: integer, nullable: true }
status: { type: string }
status: { $ref: '#/components/schemas/ContainerStatus' }
template: { type: string }
ipv4Address: { type: string, nullable: true }
macAddress: { type: string, nullable: true }
Expand All @@ -104,6 +104,24 @@ components:
services:
type: array
items: { type: object }
ContainerStatus:
type: string
description: >-
Live container status, resolved on read from Proxmox run-state and the
create job.
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;
unknown = Proxmox unreachable / no node credentials.
enum:
- running
- offline
- creating
- failed
- missing
- unknown
Node:
type: object
properties:
Expand Down
Loading
Loading