Add manual Watchtower trigger
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
<title>MischLabs Admin</title>
|
<title>MischLabs Admin</title>
|
||||||
<meta name="description" content="MischLabs dashboard administration.">
|
<meta name="description" content="MischLabs dashboard administration.">
|
||||||
<meta name="theme-color" content="#0f172a">
|
<meta name="theme-color" content="#0f172a">
|
||||||
<link rel="stylesheet" href="/style.css?v=14">
|
<link rel="stylesheet" href="/style.css?v=15">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="shell hero admin-hero">
|
<header class="shell hero admin-hero">
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
<section class="ops-card">
|
<section class="ops-card">
|
||||||
<div class="ops-card-head">
|
<div class="ops-card-head">
|
||||||
<span>Watchtower</span>
|
<span>Watchtower</span>
|
||||||
|
<button class="ghost-button mini" id="runWatchtowerButton" type="button">Jetzt pruefen</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="watchtowerStatus" class="ops-list">Noch nicht geladen.</div>
|
<div id="watchtowerStatus" class="ops-list">Noch nicht geladen.</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -80,6 +81,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/admin.js?v=5" defer></script>
|
<script src="/admin.js?v=6" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
37
admin.js
37
admin.js
@@ -19,6 +19,7 @@ const saveLocalButton = document.querySelector('#saveLocalButton');
|
|||||||
const resetLocalButton = document.querySelector('#resetLocalButton');
|
const resetLocalButton = document.querySelector('#resetLocalButton');
|
||||||
const downloadConfigButton = document.querySelector('#downloadConfigButton');
|
const downloadConfigButton = document.querySelector('#downloadConfigButton');
|
||||||
const refreshOpsButton = document.querySelector('#refreshOpsButton');
|
const refreshOpsButton = document.querySelector('#refreshOpsButton');
|
||||||
|
const runWatchtowerButton = document.querySelector('#runWatchtowerButton');
|
||||||
const diskStatus = document.querySelector('#diskStatus');
|
const diskStatus = document.querySelector('#diskStatus');
|
||||||
const watchtowerStatus = document.querySelector('#watchtowerStatus');
|
const watchtowerStatus = document.querySelector('#watchtowerStatus');
|
||||||
const containerStatus = document.querySelector('#containerStatus');
|
const containerStatus = document.querySelector('#containerStatus');
|
||||||
@@ -404,6 +405,41 @@ async function restartContainer(row) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runWatchtowerOnce() {
|
||||||
|
const confirmed = window.confirm('Watchtower jetzt manuell starten? Je nach Updates koennen Dienste kurz neu starten.');
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
const previousText = runWatchtowerButton.textContent;
|
||||||
|
runWatchtowerButton.disabled = true;
|
||||||
|
runWatchtowerButton.textContent = 'Laeuft...';
|
||||||
|
watchtowerStatus.innerHTML = '<div class="ops-line"><strong>Manueller Lauf</strong><span>Gestartet...</span></div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/watchtower/run-once', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokenSet.id_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
|
||||||
|
|
||||||
|
watchtowerStatus.innerHTML = `
|
||||||
|
<div class="ops-line"><strong>Manueller Lauf</strong><span>Gestartet</span></div>
|
||||||
|
<div class="ops-line"><strong>Container</strong><span>${payload.name}</span></div>
|
||||||
|
`;
|
||||||
|
window.clearTimeout(opsRefreshTimer);
|
||||||
|
opsRefreshTimer = window.setTimeout(refreshOps, 25000);
|
||||||
|
} catch (error) {
|
||||||
|
watchtowerStatus.textContent = `Watchtower konnte nicht gestartet werden: ${error.message}`;
|
||||||
|
} finally {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
runWatchtowerButton.disabled = false;
|
||||||
|
runWatchtowerButton.textContent = previousText;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshOps() {
|
async function refreshOps() {
|
||||||
diskStatus.textContent = 'Lade...';
|
diskStatus.textContent = 'Lade...';
|
||||||
watchtowerStatus.textContent = 'Lade...';
|
watchtowerStatus.textContent = 'Lade...';
|
||||||
@@ -476,6 +512,7 @@ resetLocalButton.addEventListener('click', () => {
|
|||||||
|
|
||||||
downloadConfigButton.addEventListener('click', downloadConfig);
|
downloadConfigButton.addEventListener('click', downloadConfig);
|
||||||
refreshOpsButton.addEventListener('click', refreshOps);
|
refreshOpsButton.addEventListener('click', refreshOps);
|
||||||
|
runWatchtowerButton.addEventListener('click', runWatchtowerOnce);
|
||||||
|
|
||||||
boot().catch((error) => {
|
boot().catch((error) => {
|
||||||
loginMessage.textContent = `Adminseite konnte nicht starten: ${error.message}`;
|
loginMessage.textContent = `Adminseite konnte nicht starten: ${error.message}`;
|
||||||
|
|||||||
56
server.js
56
server.js
@@ -11,6 +11,8 @@ const dockerPublicDir = path.join(__dirname, 'public');
|
|||||||
const PUBLIC_DIR = fs.existsSync(dockerPublicDir) ? dockerPublicDir : __dirname;
|
const PUBLIC_DIR = fs.existsSync(dockerPublicDir) ? dockerPublicDir : __dirname;
|
||||||
const DOCKER_SOCKET = process.env.DOCKER_SOCKET || '/var/run/docker.sock';
|
const DOCKER_SOCKET = process.env.DOCKER_SOCKET || '/var/run/docker.sock';
|
||||||
const WATCHTOWER_CONTAINER = process.env.WATCHTOWER_CONTAINER || 'watchtower';
|
const WATCHTOWER_CONTAINER = process.env.WATCHTOWER_CONTAINER || 'watchtower';
|
||||||
|
const WATCHTOWER_RUN_IMAGE = process.env.WATCHTOWER_RUN_IMAGE || 'containrrr/watchtower';
|
||||||
|
const WATCHTOWER_CONFIG_PATH = process.env.WATCHTOWER_CONFIG_PATH || '/volume2/docker/watchtower/config.json';
|
||||||
const AUTHORITY = process.env.AUTHORITY || 'https://auth.mischlabs.de/realms/mischlabs';
|
const AUTHORITY = process.env.AUTHORITY || 'https://auth.mischlabs.de/realms/mischlabs';
|
||||||
const ADMIN_CLIENT_ID = process.env.ADMIN_CLIENT_ID || 'mischlabs-admin';
|
const ADMIN_CLIENT_ID = process.env.ADMIN_CLIENT_ID || 'mischlabs-admin';
|
||||||
const ADMIN_USERS = (process.env.ADMIN_USERS || 'mrdiderot').split(',').map((user) => user.trim().toLowerCase()).filter(Boolean);
|
const ADMIN_USERS = (process.env.ADMIN_USERS || 'mrdiderot').split(',').map((user) => user.trim().toLowerCase()).filter(Boolean);
|
||||||
@@ -74,7 +76,8 @@ function dockerRequest(endpoint, options = {}) {
|
|||||||
const req = http.request({
|
const req = http.request({
|
||||||
socketPath: DOCKER_SOCKET,
|
socketPath: DOCKER_SOCKET,
|
||||||
path: endpoint,
|
path: endpoint,
|
||||||
method: options.method || 'GET'
|
method: options.method || 'GET',
|
||||||
|
headers: options.headers || {}
|
||||||
}, (res) => {
|
}, (res) => {
|
||||||
let body = '';
|
let body = '';
|
||||||
res.setEncoding('utf8');
|
res.setEncoding('utf8');
|
||||||
@@ -284,6 +287,42 @@ async function restartContainer(containerName) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runWatchtowerOnce() {
|
||||||
|
const name = `mischlabs-watchtower-run-once-${Date.now()}`;
|
||||||
|
const body = JSON.stringify({
|
||||||
|
Image: WATCHTOWER_RUN_IMAGE,
|
||||||
|
Cmd: ['--run-once'],
|
||||||
|
Labels: {
|
||||||
|
'com.mischlabs.role': 'manual-watchtower-run',
|
||||||
|
'com.centurylinklabs.watchtower.enable': 'false'
|
||||||
|
},
|
||||||
|
HostConfig: {
|
||||||
|
AutoRemove: true,
|
||||||
|
Binds: [
|
||||||
|
'/var/run/docker.sock:/var/run/docker.sock',
|
||||||
|
`${WATCHTOWER_CONFIG_PATH}:/config.json:ro`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const created = await dockerRequest(`/containers/create?name=${encodeURIComponent(name)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(body)
|
||||||
|
},
|
||||||
|
body
|
||||||
|
});
|
||||||
|
|
||||||
|
await dockerRequest(`/containers/${created.Id}/start`, { method: 'POST' });
|
||||||
|
return {
|
||||||
|
id: created.Id.slice(0, 12),
|
||||||
|
name,
|
||||||
|
image: WATCHTOWER_RUN_IMAGE,
|
||||||
|
startedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function getDisks() {
|
async function getDisks() {
|
||||||
const disks = [];
|
const disks = [];
|
||||||
|
|
||||||
@@ -495,6 +534,21 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.url?.match(/^\/api\/watchtower\/run-once(?:[?#].*)?$/)) {
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
json(res, 405, { error: 'Method not allowed' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await verifyAdminRequest(req);
|
||||||
|
json(res, 202, await runWatchtowerOnce());
|
||||||
|
} catch (error) {
|
||||||
|
json(res, error.statusCode || 500, { error: error.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await serveStatic(req, res);
|
await serveStatic(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user