feat(cli): add a captain-update
script to generate a report of all applications to update
This commit is contained in:
parent
640766e8a7
commit
e4a01bb96c
17
README.md
17
README.md
@ -1,3 +1,18 @@
|
||||
# Tools
|
||||
|
||||
Tools allowing to keep sans.pub up-to-date and automate common recurring actions.
|
||||
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
219
captain-update.mjs
Executable 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(", ")
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue
Block a user