|
1 | 1 | import { getApplicationDetails } from "./applications"; |
2 | 2 | import { VendorPortalApi } from "./configuration"; |
| 3 | +export interface ChannelRelease { |
| 4 | + sequence: string; |
| 5 | + channelSequence?: string; |
| 6 | + airgapBuildStatus?: string; |
| 7 | +} |
3 | 8 |
|
4 | 9 | export class Channel { |
5 | 10 | name: string; |
6 | 11 | id: string; |
7 | 12 | slug: string; |
8 | 13 | releaseSequence?: number; |
| 14 | + buildAirgapAutomatically?: boolean; |
| 15 | +} |
| 16 | + |
| 17 | +export class StatusError extends Error { |
| 18 | + statusCode: number; |
| 19 | + |
| 20 | + constructor(message: string, statusCode: number) { |
| 21 | + super(message); |
| 22 | + this.statusCode = statusCode; |
| 23 | + } |
9 | 24 | } |
10 | 25 |
|
11 | 26 | export const exportedForTesting = { |
@@ -98,11 +113,80 @@ export async function archiveChannel(vendorPortalApi: VendorPortalApi, appSlug: |
98 | 113 | async function findChannelDetailsInOutput(channels: any[], { slug, name }: ChannelIdentifier): Promise<Channel> { |
99 | 114 | for (const channel of channels) { |
100 | 115 | if (slug && channel.channelSlug == slug) { |
101 | | - return { name: channel.name, id: channel.id, slug: channel.channelSlug, releaseSequence: channel.releaseSequence }; |
| 116 | + return { name: channel.name, id: channel.id, slug: channel.channelSlug, releaseSequence: channel.releaseSequence, buildAirgapAutomatically: channel.buildAirgapAutomatically }; |
102 | 117 | } |
103 | 118 | if (name && channel.name == name) { |
104 | | - return { name: channel.name, id: channel.id, slug: channel.channelSlug, releaseSequence: channel.releaseSequence }; |
| 119 | + return { name: channel.name, id: channel.id, slug: channel.channelSlug, releaseSequence: channel.releaseSequence, buildAirgapAutomatically: channel.buildAirgapAutomatically }; |
105 | 120 | } |
106 | 121 | } |
107 | 122 | return Promise.reject({ channel: null, reason: `Could not find channel with slug ${slug} or name ${name}` }); |
108 | 123 | } |
| 124 | + |
| 125 | +export async function pollForAirgapReleaseStatus(vendorPortalApi: VendorPortalApi, appId: string, channelId: string, releaseSequence: number, expectedStatus: string, timeout: number = 120, sleeptimeMs: number = 5000): Promise<string> { |
| 126 | + // get airgapped build release from the api, look for the status of the id to be ${status} |
| 127 | + // if it's not ${status}, sleep for 5 seconds and try again |
| 128 | + // if it is ${status}, return the release with that status |
| 129 | + // iterate for timeout/sleeptime times |
| 130 | + const iterations = (timeout * 1000) / sleeptimeMs; |
| 131 | + for (let i = 0; i < iterations; i++) { |
| 132 | + try { |
| 133 | + const release = await getAirgapBuildRelease(vendorPortalApi, appId, channelId, releaseSequence); |
| 134 | + if (release.airgapBuildStatus === expectedStatus) { |
| 135 | + return release.airgapBuildStatus; |
| 136 | + } |
| 137 | + if (release.airgapBuildStatus === "failed") { |
| 138 | + console.debug(`Airgapped build release ${releaseSequence} failed`); |
| 139 | + return "failed"; |
| 140 | + } |
| 141 | + console.debug(`Airgapped build release ${releaseSequence} is not ready, sleeping for ${sleeptimeMs / 1000} seconds`); |
| 142 | + await new Promise(f => setTimeout(f, sleeptimeMs)); |
| 143 | + } catch (err) { |
| 144 | + if (err instanceof StatusError) { |
| 145 | + if (err.statusCode >= 500) { |
| 146 | + // 5xx errors are likely transient, so we should retry |
| 147 | + console.debug(`Got HTTP error with status ${err.statusCode}, sleeping for ${sleeptimeMs / 1000} seconds`); |
| 148 | + await new Promise(f => setTimeout(f, sleeptimeMs)); |
| 149 | + } else { |
| 150 | + console.debug(`Got HTTP error with status ${err.statusCode}, exiting`); |
| 151 | + throw err; |
| 152 | + } |
| 153 | + } else { |
| 154 | + throw err; |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | + throw new Error(`Airgapped build release ${releaseSequence} did not reach status ${expectedStatus} in ${timeout} seconds`); |
| 159 | +} |
| 160 | + |
| 161 | +export async function getDownloadUrlAirgapBuildRelease(vendorPortalApi: VendorPortalApi, appId: string, channelId: string, releaseSequence: number): Promise<string> { |
| 162 | + const release = await getAirgapBuildRelease(vendorPortalApi, appId, channelId, releaseSequence); |
| 163 | + const http = await vendorPortalApi.client(); |
| 164 | + const uri = `${vendorPortalApi.endpoint}/app/${appId}/channel/${channelId}/airgap/download-url?channelSequence=${release.channelSequence}`; |
| 165 | + const res = await http.get(uri); |
| 166 | + |
| 167 | + if (res.message.statusCode != 200) { |
| 168 | + // discard the response body |
| 169 | + await res.readBody(); |
| 170 | + throw new Error(`Failed to get airgap build release: Server responded with ${res.message.statusCode}`); |
| 171 | + } |
| 172 | + const body: any = JSON.parse(await res.readBody()); |
| 173 | + return body.url; |
| 174 | +} |
| 175 | + |
| 176 | +async function getAirgapBuildRelease(vendorPortalApi: VendorPortalApi, appId: string, channelId: string, releaseSequence: number): Promise<ChannelRelease> { |
| 177 | + const http = await vendorPortalApi.client(); |
| 178 | + const uri = `${vendorPortalApi.endpoint}/app/${appId}/channel/${channelId}/releases`; |
| 179 | + const res = await http.get(uri); |
| 180 | + if (res.message.statusCode != 200) { |
| 181 | + // discard the response body |
| 182 | + await res.readBody(); |
| 183 | + throw new Error(`Failed to get airgap build release: Server responded with ${res.message.statusCode}`); |
| 184 | + } |
| 185 | + const body: any = JSON.parse(await res.readBody()); |
| 186 | + const release = body.releases.find((r: any) => r.sequence === releaseSequence); |
| 187 | + return { |
| 188 | + sequence: release.sequence, |
| 189 | + channelSequence: release.channelSequence, |
| 190 | + airgapBuildStatus: release.airgapBuildStatus |
| 191 | + }; |
| 192 | +} |
0 commit comments