Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions create-a-container/migrations/20251230144745-create-templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Templates', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
displayName: {
type: Sequelize.STRING,
allowNull: false,
comment: 'Human-readable template name'
},
proxmoxTemplateName: {
type: Sequelize.STRING,
allowNull: false,
comment: 'Proxmox template identifier (e.g., local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst)'
},
siteId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'Sites',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
createdAt: {
type: Sequelize.DATE,
allowNull: false
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false
}
});
},

async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Templates');
}
};
6 changes: 6 additions & 0 deletions create-a-container/models/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ module.exports = (sequelize, DataTypes) => {
foreignKey: 'siteId',
as: 'externalDomains'
});

// A Site has many Templates
Site.hasMany(models.Template, {
foreignKey: 'siteId',
as: 'templates'
});
}
}
Site.init({
Expand Down
42 changes: 42 additions & 0 deletions create-a-container/models/template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Template extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// A Template belongs to a Site
Template.belongsTo(models.Site, {
foreignKey: 'siteId',
as: 'site'
});
}
}
Template.init({
displayName: {
type: DataTypes.STRING,
allowNull: false
},
proxmoxTemplateName: {
type: DataTypes.STRING,
allowNull: false
},
siteId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'Sites',
key: 'id'
}
}
}, {
sequelize,
modelName: 'Template',
});
return Template;
};
83 changes: 36 additions & 47 deletions create-a-container/routers/containers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,25 @@ const express = require('express');
const router = express.Router({ mergeParams: true }); // Enable access to :siteId param
const https = require('https');
const dns = require('dns').promises;
const { Container, Service, HTTPService, TransportService, DnsService, Node, Site, ExternalDomain, Sequelize, sequelize } = require('../models');
const { Container, Service, HTTPService, TransportService, DnsService, Node, Site, ExternalDomain, Template, Sequelize, sequelize } = require('../models');
const { requireAuth } = require('../middlewares');
const ProxmoxApi = require('../utils/proxmox-api');
const serviceMap = require('../data/services.json');

// GET /sites/:siteId/containers/new - Display form for creating a new container
router.get('/new', requireAuth, async (req, res) => {
// verify site exists
const siteId = parseInt(req.params.siteId, 10);
const site = await Site.findByPk(siteId);
if (!site) {
req.flash('error', 'Site not found');
return res.redirect('/sites');
}

// Get valid container templates from all nodes in this site
const templates = [];
const nodes = await Node.findAll({
where: {
[Sequelize.Op.and]: {
siteId,
apiUrl: { [Sequelize.Op.ne]: null },
tokenId: { [Sequelize.Op.ne]: null },
secret: { [Sequelize.Op.ne]: null }
}
},
const templates = await Template.findAll({
where: { siteId },
order: [['displayName', 'ASC']]
});

// TODO: use datamodel backed templates instead of querying Proxmox here
for (const node of nodes) {
const client = new ProxmoxApi(node.apiUrl, node.tokenId, node.secret, {
httpsAgent: new https.Agent({
rejectUnauthorized: node.tlsVerify !== false
})
});

// Get datastores for this node
const datastores = await client.datastores(node.name, 'vztmpl', true);

// Iterate over each datastore and get its contents
for (const datastore of datastores) {
const contents = await client.storageContents(node.name, datastore.storage, 'vztmpl');

// Add templates from this storage
for (const item of contents) {
templates.push({
volid: item.volid,
name: item.volid.split('/').pop(), // Extract filename from volid
size: item.size,
node: node.name,
storage: datastore.storage
});
}
}
}

// Get external domains for this site
const externalDomains = await ExternalDomain.findAll({
where: { siteId },
order: [['name', 'ASC']]
Expand All @@ -68,7 +30,7 @@ router.get('/new', requireAuth, async (req, res) => {
site,
templates,
externalDomains,
container: undefined, // Not editing
container: undefined,
req
});
});
Expand Down Expand Up @@ -217,10 +179,37 @@ router.post('/', async (req, res) => {

// TODO: build the container async in a Job
try {
// clone the template
const { hostname, template, services } = req.body;
const [ nodeName, ostemplate ] = template.split(',');
const node = await Node.findOne({ where: { name: nodeName, siteId } });
const { hostname, templateId, services } = req.body;

const template = await Template.findOne({
where: { id: templateId, siteId }
});

if (!template) {
req.flash('error', 'Template not found');
return res.redirect(`/sites/${siteId}/containers/new`);
}

const ostemplate = template.proxmoxTemplateName;
const [ storage ] = ostemplate.split(':');

const nodes = await Node.findAll({
where: {
[Sequelize.Op.and]: {
siteId,
apiUrl: { [Sequelize.Op.ne]: null },
tokenId: { [Sequelize.Op.ne]: null },
secret: { [Sequelize.Op.ne]: null }
}
},
});

if (nodes.length === 0) {
req.flash('error', 'No configured nodes available for this site');
return res.redirect(`/sites/${siteId}/containers/new`);
}

const node = nodes[0];
const client = new ProxmoxApi(node.apiUrl, node.tokenId, node.secret, {
httpsAgent: new https.Agent({
rejectUnauthorized: node.tlsVerify !== false
Expand Down
2 changes: 2 additions & 0 deletions create-a-container/routers/sites.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,11 @@ router.use('/:siteId', setCurrentSite);
const nodesRouter = require('./nodes');
const containersRouter = require('./containers');
const externalDomainsRouter = require('./external-domains');
const templatesRouter = require('./templates');
router.use('/:siteId/nodes', nodesRouter);
router.use('/:siteId/containers', containersRouter);
router.use('/:siteId/external-domains', externalDomainsRouter);
router.use('/:siteId/templates', templatesRouter);

// GET /sites - List all sites (available to all authenticated users)
router.get('/', async (req, res) => {
Expand Down
Loading
Loading