#!/usr/bin/env zx $.verbose = argv.v || argv.verbose; if (argv.help) { console.log(`${chalk.white.bgBlue( "🐿️ captain-update.mjs - Check for updates of all deployed images on CapRover" )} Usage: ${chalk.yellow( "CAPTAIN_DOMAIN=captain.my.example.com CAPTAIN_PASSWORD='This is super S3cure!' ./captain-update.mjs [options]" )} ${chalk.grey( "CAPTAIN_DOMAIN must have the domain name of the CapRover instance" )} ${chalk.grey( "CAPTAIN_PASSWORD must have the password of the CapRover instance" )} Optional options: ${chalk.yellow("--help")} Display this help ${chalk.yellow("-v --verbose")} Verbose output `); process.exit(0); } const hasAllEnvVars = process.env.CAPTAIN_DOMAIN && process.env.CAPTAIN_PASSWORD; if (!hasAllEnvVars) { console.error( `Missing ${chalk.red("CAPTAIN_DOMAIN")} or ${chalk.red( "CAPTAIN_PASSWORD" )} environment variables. Use ${chalk.green("--help")} for help.` ); process.exit(1); } const API_URL = `https://${process.env.CAPTAIN_DOMAIN}/api/v2`; const authResponse = await fetch(`${API_URL}/login`, { method: "POST", headers: { "Content-Type": "application/json", "x-namespace": "captain", }, body: JSON.stringify({ password: process.env.CAPTAIN_PASSWORD, }), }).then((res) => res.json()); const authToken = authResponse?.data?.token; if (!authToken) { console.error(`Failed to authenticate at ${API_URL}`); process.exit(1); } const fetchAuthenticatedApi = async (url, method = "GET", body = undefined) => { const response = await fetch(`${API_URL}${url}`, { method, headers: { "Content-Type": "application/json", "x-namespace": "captain", "x-captain-auth": authToken, }, body: body ? JSON.stringify(body) : undefined, }).then((res) => res.json()); return response; }; const applicationsResponse = await fetchAuthenticatedApi( "/user/apps/appDefinitions" ); console.log( chalk.green( `Found ${applicationsResponse.data.appDefinitions.length} applications on ${applicationsResponse.data.rootDomain}` ) ); const appStates = await Promise.all( applicationsResponse.data.appDefinitions.map(async (app) => { const currentVersion = app.versions[app.deployedVersion]; const [imageName, currentTag] = currentVersion.deployedImageName.split(":"); const imagePath = imageName.includes("/") ? imageName : `library/${imageName}`; let appState = { name: app.appName, lastDeployed: currentVersion.timeStamp, currentImage: { name: currentVersion.deployedImageName, dockerHubUrl: imageName.includes("/") ? `https://hub.docker.com/r/${imageName}` : `https://hub.docker.com/_/${imageName}`, image: imageName, tag: currentTag, }, }; const tagsResponse = await fetch( `https://hub.docker.com/v2/repositories/${imagePath}/tags?page_size=10&page=1&ordering=last_updated` ) .then((res) => res.json()) .catch((error) => { console.error( "Impossible to get Docker tags for image", chalk.red(imagePath) ); return {}; }); const tags = tagsResponse.results; if (!tags || tags.length === 0) { return appState; } const latestTag = tags.find((tag) => tag.name === "latest"); const latestTags = tags .filter((tag) => { if (!latestTag) { return true; } return tag.digest === latestTag.digest; }) .map((tag) => tag.name); const isLatest = latestTags.includes(currentTag); appState.isLatest = latestTag && isLatest; appState.latestTags = latestTags; appState.recommendedVersion = `${imageName}:${latestTags.pop()}`; return appState; }) ); const appsUpToDate = appStates.filter((app) => app.isLatest); const appsUnknown = appStates.filter((app) => !app.recommendedVersion); const appsOutdated = appStates.filter( (app) => app.recommendedVersion && !app.isLatest ); console.log( "\n", chalk.green(`# Applications report (${new Date().toISOString()})`), "\n\n" ); console.log(chalk.green("## Applications up-to-date", "\n")); console.log( chalk.grey( "These applications seems up-to-date, congrats! Please check for unsupported software not updated recently, and improve this script if possible." ), "\n" ); appsUpToDate .sort((a, b) => new Date(a.lastDeployed) - new Date(b.lastDeployed)) .forEach((app) => { console.log( "- [x]", chalk.blue(app.name), `: ${app.currentImage.name}`, "\n\t", "- current:", app.currentImage.name, "\n\t", "- last deployed:", app.lastDeployed ); }); console.log("\n\n", chalk.green("## Applications unknown", "\n")); console.log( chalk.grey( "I couldn't automatically find the latest version for these apps. Please check manually, and improve this script if possible." ), "\n" ); appsUnknown.forEach((app) => { console.log( "- [ ]", chalk.blue(app.name), `: ${app.currentImage.name} -> TODO`, "\n\t", "- current:", app.currentImage.name, "\n\t", "- docker hub URL:", app.currentImage.dockerHubUrl ); }); console.log("\n\n", chalk.green("## Applications outdated", "\n")); console.log( chalk.grey( "These applications seems outdated. Please try to update to their latest versions, and improve this script if possible." ), "\n" ); appsOutdated.forEach((app) => { console.log( "- [ ]", chalk.blue(app.name), `: ${app.currentImage.name} -> ${app.recommendedVersion}`, "\n\t", "- current:", app.currentImage.name, "\n\t", "- recommended:", app.recommendedVersion, "\n\t", "- docker hub URL:", app.currentImage.dockerHubUrl, "\n\t", "- tags:", app.latestTags.join(", ") ); });