-
Notifications
You must be signed in to change notification settings - Fork 0
feat(release): add polling status for airgap build release #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
1a0cee2
518d35b
1f589c8
c60f50e
f09e1e4
264a651
230693d
731dacb
c52de2e
b87bfd6
d24ada6
4d4e960
6f527c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // Example script to test the pollForAirgapReleaseStatus function | ||
| // Usage: node poll-for-airgap-build.js <appId> <channelId> <releaseSequence> <expectedStatus> | ||
|
|
||
| import { VendorPortalApi } from "../dist/configuration"; | ||
| import { pollForAirgapReleaseStatus } from "../dist/releases"; | ||
| import * as readline from 'readline'; | ||
|
|
||
| // Function to get input from the user | ||
| async function getUserInput(prompt: string): Promise<string> { | ||
| const rl = readline.createInterface({ | ||
| input: process.stdin, | ||
| output: process.stdout | ||
| }); | ||
|
|
||
| return new Promise((resolve) => { | ||
| rl.question(prompt, (answer) => { | ||
| rl.close(); | ||
| resolve(answer); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| async function main() { | ||
| try { | ||
| // Initialize the API client | ||
| const api = new VendorPortalApi(); | ||
|
|
||
| // Get API token from environment variable | ||
| api.apiToken = process.env.REPLICATED_API_TOKEN || ""; | ||
|
|
||
| if (!api.apiToken) { | ||
| throw new Error("REPLICATED_API_TOKEN environment variable is not set"); | ||
| } | ||
|
|
||
| // Get parameters from command line arguments or prompt for them | ||
| let appId = process.argv[2]; | ||
| let channelId = process.argv[3]; | ||
| let releaseSequence = process.argv[4] ? parseInt(process.argv[4]) : undefined; | ||
| let expectedStatus = process.argv[5]; | ||
|
|
||
| // If any parameters are missing, prompt for them | ||
| if (!appId) { | ||
| appId = await getUserInput("Enter Application ID: "); | ||
| } | ||
|
|
||
| if (!channelId) { | ||
| channelId = await getUserInput("Enter Channel ID: "); | ||
| } | ||
|
|
||
| if (!releaseSequence) { | ||
| const sequenceStr = await getUserInput("Enter Release Sequence: "); | ||
| releaseSequence = parseInt(sequenceStr); | ||
| } | ||
|
|
||
| if (!expectedStatus) { | ||
| expectedStatus = await getUserInput("Enter Expected Status (e.g., 'built', 'warn', 'metadata'): "); | ||
| } | ||
|
|
||
| // Validate inputs | ||
| if (isNaN(releaseSequence)) { | ||
| throw new Error("Release Sequence must be a number"); | ||
| } | ||
|
|
||
| console.log(`\nPolling for airgap release status with the following parameters:`); | ||
| console.log(`- Application ID: ${appId}`); | ||
| console.log(`- Channel ID: ${channelId}`); | ||
| console.log(`- Release Sequence: ${releaseSequence}`); | ||
| console.log(`- Expected Status: ${expectedStatus}`); | ||
| console.log(`\nThis will poll until the release reaches the expected status or times out.`); | ||
|
|
||
| console.log("\nStarting to poll for airgap release status..."); | ||
|
|
||
| const status = await pollForAirgapReleaseStatus( | ||
| api, | ||
| appId, | ||
| channelId, | ||
| releaseSequence, | ||
| expectedStatus, | ||
| 60, // 1 minute timeout | ||
| 1000 // 1 second polling interval | ||
| ); | ||
|
|
||
| console.log(`\nSuccess! Release ${releaseSequence} has reached status: ${status}`); | ||
| } catch (error) { | ||
| console.error(`\nError: ${error.message}`); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| // Run the main function | ||
| main(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ import { zonedTimeToUtc } from "date-fns-tz"; | |
| export interface Release { | ||
| sequence: string; | ||
| charts?: ReleaseChart[]; | ||
| airgapBuildStatus?: string; | ||
|
||
| } | ||
|
|
||
| export interface ReleaseChart { | ||
|
|
@@ -45,6 +46,15 @@ export const exportedForTesting = { | |
| reportCompatibilityResultByAppId | ||
| }; | ||
|
|
||
| export class StatusError extends Error { | ||
| statusCode: number; | ||
|
|
||
| constructor(message: string, statusCode: number) { | ||
| super(message); | ||
| this.statusCode = statusCode; | ||
| } | ||
| } | ||
|
|
||
| export async function createRelease(vendorPortalApi: VendorPortalApi, appSlug: string, yamlDir: string): Promise<Release> { | ||
| const http = await vendorPortalApi.client(); | ||
|
|
||
|
|
@@ -341,3 +351,56 @@ async function reportCompatibilityResultByAppId(vendorPortalApi: VendorPortalApi | |
| // discard the response body | ||
| await res.readBody(); | ||
| } | ||
|
|
||
| export async function pollForAirgapReleaseStatus(vendorPortalApi: VendorPortalApi, appId: string, channelId: string, releaseSequence: number, expectedStatus: string, timeout: number = 120, sleeptimeMs: number = 5000): Promise<string> { | ||
| // get airgapped build release from the api, look for the status of the id to be ${status} | ||
| // if it's not ${status}, sleep for 5 seconds and try again | ||
| // if it is ${status}, return the release with that status | ||
| // iterate for timeout/sleeptime times | ||
| const iterations = (timeout * 1000) / sleeptimeMs; | ||
| for (let i = 0; i < iterations; i++) { | ||
| try { | ||
| const release = await getAirgapBuildRelease(vendorPortalApi, appId, channelId, releaseSequence); | ||
| if (release.airgapBuildStatus === expectedStatus) { | ||
| return release.airgapBuildStatus; | ||
| } | ||
| if (release.airgapBuildStatus === "failed") { | ||
| console.debug(`Airgapped build release ${releaseSequence} failed`); | ||
| return "failed"; | ||
| } | ||
| console.debug(`Airgapped build release ${releaseSequence} is not ready, sleeping for ${sleeptimeMs / 1000} seconds`); | ||
| await new Promise(f => setTimeout(f, sleeptimeMs)); | ||
| } catch (err) { | ||
| if (err instanceof StatusError) { | ||
| if (err.statusCode >= 500) { | ||
| // 5xx errors are likely transient, so we should retry | ||
| console.debug(`Got HTTP error with status ${err.statusCode}, sleeping for ${sleeptimeMs / 1000} seconds`); | ||
| await new Promise(f => setTimeout(f, sleeptimeMs)); | ||
| } else { | ||
| console.debug(`Got HTTP error with status ${err.statusCode}, exiting`); | ||
| throw err; | ||
| } | ||
| } else { | ||
| throw err; | ||
| } | ||
| } | ||
| } | ||
| throw new Error(`Airgapped build release ${releaseSequence} did not reach status ${expectedStatus} in ${timeout} seconds`); | ||
| } | ||
|
|
||
| async function getAirgapBuildRelease(vendorPortalApi: VendorPortalApi, appId: string, channelId: string, releaseSequence: number): Promise<Release> { | ||
| const http = await vendorPortalApi.client(); | ||
| const uri = `${vendorPortalApi.endpoint}/app/${appId}/channel/${channelId}/releases`; | ||
| const res = await http.get(uri); | ||
| if (res.message.statusCode != 200) { | ||
| // discard the response body | ||
| await res.readBody(); | ||
| throw new Error(`Failed to get airgap build release: Server responded with ${res.message.statusCode}`); | ||
| } | ||
| const body: any = JSON.parse(await res.readBody()); | ||
| const release = body.releases.find((r: any) => r.sequence === releaseSequence); | ||
| return { | ||
| sequence: release.sequence, | ||
| airgapBuildStatus: release.airgapBuildStatus | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the changes be in channels.ts, as this is about a release promoted to a channel and not an "Application" release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. I have moved the function back to channels.ts