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
15
README.md
15
README.md
@ -1,3 +1,18 @@
|
|||||||
# Tools
|
# 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