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 { const res = await this.fetch("/user/apps/appDefinitions"); return ( res.data?.appDefinitions?.map(Application.createFromDefinition) ?? [] ); } async updateApplication(appName: string, version: string): Promise { console.debug("Caprover: Updating application", appName, "to", version); try { const response = await this.fetch(`/user/apps/appData/${appName}`, { method: "POST", body: JSON.stringify({ captainDefinitionContent: JSON.stringify({ schemaVersion: 2, imageName: version, }), gitHash: "", }), }); if (response.status !== 100) { throw new Error(response.description); } } catch (error) { throw new Error( `Failed to update application ${appName} to ${version}: ${error ?? ""}.` ); } this.eventStore.append( new ApplicationUpdateStarted( { id: appName, newVersion: version }, new Date() ) ); } async getLogs(appName: string): Promise { console.debug("Caprover: Fetching logs for", appName); const response = await this.fetch(`/user/apps/appData/${appName}/logs`); if (response.status !== 100) { throw new Error( `Failed to fetch logs for application ${appName}: ${response.description}` ); } return response.data.logs; } 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); const headers = new Headers(options?.headers); headers.set("Content-Type", "application/json"); headers.set("x-namespace", "captain"); headers.set("x-captain-auth", this.authToken); return fetch(`${this.apiUrl}${path}`, { ...options, headers: headers, }).then((res) => { console.debug( "\t<- Caprover Response", options?.method, path, res.status, res.statusText ); return res.json(); }); } } export { Application }; export default Caprover;