Proper Template Support for Stacks
Release rolling / release (push) Has been cancelled

This commit is contained in:
Raven Scott
2026-07-16 04:09:50 -04:00
parent fb8f1c4fa4
commit 4bbbd45f42
4 changed files with 430 additions and 69 deletions
+122 -51
View File
@@ -3535,8 +3535,10 @@ function renderStacks(stacks) {
function setupDeployStackHandler() {
const deployStackBtn = document.getElementById('deploy-stack-btn');
const deployStackForm = document.getElementById('deploy-stack-form');
if (deployStackBtn && deployStackForm) {
const gitOpsBtn = document.getElementById('stack-gitops-btn');
if (deployStackBtn && deployStackForm && deployStackBtn.dataset.handlerBound !== '1') {
deployStackBtn.dataset.handlerBound = '1';
deployStackBtn.addEventListener('click', async () => {
const stackName = document.getElementById('stack-name').value.trim();
const composeContent = document.getElementById('compose-content').value.trim();
@@ -3574,41 +3576,44 @@ function setupDeployStackHandler() {
});
}
document.getElementById('stack-gitops-btn')?.addEventListener('click', async () => {
const stackName = document.getElementById('stack-name')?.value?.trim();
const repoUrl = document.getElementById('stack-git-url')?.value?.trim();
const ref = document.getElementById('stack-git-ref')?.value?.trim() || 'main';
const composePath =
document.getElementById('stack-git-path')?.value?.trim() || 'docker-compose.yml';
if (!stackName || !repoUrl) {
showAlert('warning', 'Stack name and repository URL are required for GitOps sync');
return;
}
showStatusIndicator(`GitOps sync ${stackName} from ${repoUrl}`);
try {
const response = await manager.request(Methods.syncStackFromGit, {
stackName,
repoUrl,
ref,
composePath,
});
if (response?.composeContent == null && response?.success) {
// deployed server-side
if (gitOpsBtn && gitOpsBtn.dataset.handlerBound !== '1') {
gitOpsBtn.dataset.handlerBound = '1';
gitOpsBtn.addEventListener('click', async () => {
const stackName = document.getElementById('stack-name')?.value?.trim();
const repoUrl = document.getElementById('stack-git-url')?.value?.trim();
const ref = document.getElementById('stack-git-ref')?.value?.trim() || 'main';
const composePath =
document.getElementById('stack-git-path')?.value?.trim() || 'docker-compose.yml';
if (!stackName || !repoUrl) {
showAlert('warning', 'Stack name and repository URL are required for GitOps sync');
return;
}
showAlert(
'success',
response?.message || `Stack "${stackName}" synced from git`
);
const modal = bootstrap.Modal.getInstance(document.getElementById('deploy-stack-modal'));
if (modal) modal.hide();
navigateToView('stacks');
loadStacks();
} catch (error) {
presentError(error, 'syncStackFromGit', { showAlert });
} finally {
hideStatusIndicator();
}
});
showStatusIndicator(`GitOps sync ${stackName} from ${repoUrl}`);
try {
const response = await manager.request(Methods.syncStackFromGit, {
stackName,
repoUrl,
ref,
composePath,
});
if (response?.composeContent == null && response?.success) {
// deployed server-side
}
showAlert(
'success',
response?.message || `Stack "${stackName}" synced from git`
);
const modal = bootstrap.Modal.getInstance(document.getElementById('deploy-stack-modal'));
if (modal) modal.hide();
navigateToView('stacks');
loadStacks();
} catch (error) {
presentError(error, 'syncStackFromGit', { showAlert });
} finally {
hideStatusIndicator();
}
});
}
}
// Subscription for volumes store to auto-update UI
@@ -7580,18 +7585,70 @@ function setupDeployResourceSliders() {
}
window.setupDeployResourceSliders = setupDeployResourceSliders;
/**
* Open the Stacks deploy modal pre-filled from a Portainer type 2/3 template.
* @param {object} resolved after resolveTemplateForDeploy
* @returns {boolean}
*/
function openStackDeployFromTemplate(resolved) {
const build =
typeof window.buildStackDeployPayload === 'function'
? window.buildStackDeployPayload
: null;
if (!build) return false;
const payload = build(resolved);
if (!payload?.ok) {
showAlert('warning', payload?.error || 'Could not build stack deploy payload');
return false;
}
setupDeployStackHandler();
const nameEl = document.getElementById('stack-name');
const composeEl = document.getElementById('compose-content');
const envEl = document.getElementById('stack-env-file');
const gitUrlEl = document.getElementById('stack-git-url');
const gitRefEl = document.getElementById('stack-git-ref');
const gitPathEl = document.getElementById('stack-git-path');
if (nameEl) nameEl.value = payload.stackName;
if (composeEl) composeEl.value = payload.composeContent;
if (envEl) envEl.value = payload.envFileContent || '';
if (gitUrlEl) gitUrlEl.value = payload.repoUrl || '';
if (gitRefEl) gitRefEl.value = payload.ref || 'main';
if (gitPathEl) gitPathEl.value = payload.composePath || 'docker-compose.yml';
// Optional note strip at top of modal body
const modalBody = document.querySelector('#deploy-stack-modal .modal-body');
if (modalBody) {
modalBody.querySelector('.stack-template-note')?.remove();
if (payload.note) {
const note = document.createElement('div');
note.className = 'stack-template-note alert alert-info py-2 px-3 mb-3 small';
note.innerHTML = `<div class="fw-semibold mb-1"><i class="fas fa-info-circle me-1"></i>${escapeHtmlLite(resolved.title || payload.stackName)} notes</div><div>${payload.note}</div>`;
modalBody.insertBefore(note, modalBody.firstChild);
}
}
navigateToView('stacks');
const modalEl = document.getElementById('deploy-stack-modal');
if (modalEl && typeof bootstrap !== 'undefined') {
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
}
const svcN = payload.serviceCount || 0;
showAlert(
'info',
`Loaded stack template “${resolved.title || payload.stackName}${svcN ? ` (${svcN} services)` : ''} — review compose and deploy.`
);
return true;
}
window.openStackDeployFromTemplate = openStackDeployFromTemplate;
async function selectTemplateForDeploy(template) {
if (!template || typeof template !== 'object') {
showAlert('danger', 'Invalid template');
return;
}
const formSection = document.getElementById('deploy-form-section');
if (formSection) {
formSection.style.display = 'block';
formSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
initTemplateDeployer();
setupDeployViewFormHandler();
setupDeployResourceSliders();
@@ -7601,9 +7658,7 @@ async function selectTemplateForDeploy(template) {
if (typeof window.setDeployFormScope === 'function') {
window.setDeployFormScope(form);
}
if (form) form.reset();
// Full Portainer template → form mapping (ports, volumes, env, network, labels, …)
const populate =
typeof window.populateDeployFormFromTemplate === 'function'
? window.populateDeployFormFromTemplate
@@ -7618,13 +7673,16 @@ async function selectTemplateForDeploy(template) {
return;
}
const typeNum = Number(template.type);
const isCatalogStack =
typeNum === 2 ||
typeNum === 3 ||
(!String(template.image || '').trim() &&
(template.repository?.url || template.repository));
let resolved = template;
const needsCompose =
!String(template.image || template.Image || '').trim() &&
(Number(template.type) === 2 ||
Number(template.type) === 3 ||
template.repository);
if (needsCompose && resolve) {
// Always resolve stack catalogs (and type-1 never needs compose)
if (isCatalogStack && resolve) {
showStatusIndicator(
`Loading compose for ${template.title || template.name || 'template'}`
);
@@ -7635,12 +7693,25 @@ async function selectTemplateForDeploy(template) {
}
}
// Type 2/3 (and multi-service compose) → full stack deploy path (Portainer parity)
if (isCatalogStack && resolved._composeText) {
if (openStackDeployFromTemplate(resolved)) return;
}
// Container path (type 1, or stack fallback when compose missing but image resolved)
const formSection = document.getElementById('deploy-form-section');
if (formSection) {
formSection.style.display = 'block';
formSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
if (form) form.reset();
const result = populate(resolved);
if (result?.isStack) {
showAlert(
'warning',
result.warnings?.[0] ||
`${template.title || template.name || 'Template'}” is a Compose/stack template. Use Stacks with the compose file from its repository, or pick a container (type 1) template.`
`${template.title || template.name || 'Template'}” is a Compose/stack template. Could not load its compose file — check the repository URL or deploy manually under Stacks.`
);
} else if (result?.warnings?.length) {
showAlert(resolved._composeResolved ? 'info' : 'warning', result.warnings.join(' '));