You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

159 lines
4.1 KiB
TypeScript

import EventStore from "../domain/EventStore";
import { ApplicationUpdateStarted } from "../domain/events/ApplicationUpdateStarted";
type TODO_TypeDefinition = any;
class Application {
private constructor(private readonly data: any) {}
static createFromDefinition(data: any): Application {
return new Application(data);
}
get name(): string {
return this.data.appName;
}
get lastDeployedAt(): Date {
return new Date(this.currentVersion.timeStamp);
}
get imageName(): string {
return this.currentVersion.deployedImageName;
}
get dockerImage(): undefined | { name: string; tag: string; hubUrl: string } {
const match = this.imageName.match(/^(.*):(.*)$/);
if (!match || match[1].includes("/captain/")) {
return undefined;
}
const name = match[1];
return {
name: name,
tag: match[2],
hubUrl: name.includes("/")
? `https://hub.docker.com/r/${name}`
: `https://hub.docker.com/_/${name}`,
};
}
isOlderThan(days: number): boolean {
const daysInMs = days * 24 * 60 * 60 * 1000;
const now = Date.now();
return now - this.lastDeployedAt.getTime() > daysInMs;
}
toString(): string {
return JSON.stringify(this.data, null, 2);
}
private get currentVersion(): TODO_TypeDefinition {
return this.data.versions.find((version: { version: number }) => {
return version.version === this.data.deployedVersion;
});
}
}
class Caprover {
private authToken: string = "";
private readonly apiUrl: string;
private readonly eventStore: EventStore;
constructor(
eventStore: EventStore,
readonly domain?: string,
private readonly password?: string
) {
if (!domain || !password) {
throw new Error("Missing domain or password");
}
this.apiUrl = `https://${domain}/api/v2`;
this.eventStore = eventStore;
}
async getApps(): Promise<Application[]> {
const res = await this.fetch("/user/apps/appDefinitions");
return (
res.data?.appDefinitions?.map(Application.createFromDefinition) ?? []
);
}
async updateApplication(appName: string, version: string): Promise<void> {
console.debug("Caprover: Updating application", appName, "to", version);
const response = await this.fetch(`/user/apps/appData/${appName}`, {
method: "POST",
body: JSON.stringify({
captainDefinitionContent: JSON.stringify({
schemaVersion: 2,
imageName: version,
}),
gitHash: "",
}),
});
console.debug("update application response", response);
if (response.status !== 100) {
throw new Error(
`Failed to update application ${appName} to ${version}: ${response.description}.`
);
}
this.eventStore.append(
new ApplicationUpdateStarted(
{ id: appName, newVersion: version },
new Date()
)
);
}
private async authenticate() {
if (this.authToken) {
return;
}
console.debug("Trying to authenticate at", this.apiUrl);
const authResponse = await fetch(`${this.apiUrl}/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-namespace": "captain",
},
body: JSON.stringify({
password: this.password,
}),
}).then((res) => res.json());
this.authToken = authResponse?.data?.token;
if (!this.authToken) {
throw new Error(`Failed to authenticate at ${this.apiUrl}`);
}
console.debug("Authenticated successfully at", this.apiUrl, this.authToken);
}
private async fetch(path: string, options?: RequestInit) {
await this.authenticate();
console.debug("-> Caprover Fetching", options?.method, path);
return fetch(`${this.apiUrl}${path}`, {
...options,
headers: {
"Content-Type": "application/json",
...(options?.headers || {}),
"x-namespace": "captain",
"x-captain-auth": this.authToken,
},
}).then((res) => {
console.debug(
"\t<- Caprover Response",
options?.method,
path,
res.status,
res.statusText
);
return res.json();
});
}
}
export { Application };
export default Caprover;