feat(cli): add a captain-update script to generate a report of all applications to update

This commit is contained in:
Pierre Martin 2023-05-08 09:51:48 +02:00
parent 640766e8a7
commit e4a01bb96c
2 changed files with 235 additions and 1 deletions

View File

@ -1,3 +1,18 @@
# Tools
Tools allowing to keep sans.pub up-to-date and automate common recurring actions.
## `captain-update.mjs`
Allow to generate a report of all versions of applications deployed on sans.pub, with the current status of each application and whether they need an update or not.
It can serve as a base for a changelog entry, and as a TODO list for updating applications.
Example:
```shell
CAPTAIN_DOMAIN=xxxx CAPTAIN_PASSWORD='yyy' ./captain-update.mjs > /path/to/site/src/changelog/2023-05-08.md
```
### TODO
- [ ] try to update applications automatically from a prompt

219
captain-update.mjs Executable file
View File

@ -0,0 +1,219 @@
#!/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(", ")
);
});