Skip to content

Commit 731dacb

Browse files
committed
move airgap pull to channel.ts
1 parent 230693d commit 731dacb

File tree

6 files changed

+96
-94
lines changed

6 files changed

+96
-94
lines changed

examples/poll-for-airgap-build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Usage: node poll-for-airgap-build.js <appId> <channelId> <releaseSequence> <expectedStatus>
33

44
import { VendorPortalApi } from "../dist/configuration";
5-
import { pollForAirgapReleaseStatus } from "../dist/releases";
5+
import { pollForAirgapReleaseStatus } from "../dist/channels";
66
import * as readline from 'readline';
77

88
// Function to get input from the user

src/channels.spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Interaction } from "@pact-foundation/pact";
2-
import { exportedForTesting } from "./channels";
2+
import { exportedForTesting, pollForAirgapReleaseStatus } from "./channels";
33
import { VendorPortalApi } from "./configuration";
4+
import * as mockttp from "mockttp";
45

56
const getChannelByApplicationId = exportedForTesting.getChannelByApplicationId;
67
const findChannelDetailsInOutput = exportedForTesting.findChannelDetailsInOutput;
@@ -81,3 +82,30 @@ describe("ChannelsService", () => {
8182
});
8283
});
8384
});
85+
86+
describe("pollForAirgapReleaseStatus", () => {
87+
const mockServer = mockttp.getLocal();
88+
const apiClient = new VendorPortalApi();
89+
apiClient.apiToken = "abcd1234";
90+
apiClient.endpoint = "http://localhost:8080";
91+
// Start your mock server
92+
beforeEach(() => {
93+
mockServer.start(8080);
94+
});
95+
afterEach(() => mockServer.stop());
96+
97+
it("poll for airgapped release status", async () => {
98+
const data = {
99+
releases: [
100+
{
101+
sequence: 0,
102+
airgapBuildStatus: "built"
103+
}
104+
]
105+
};
106+
await mockServer.forGet("/app/1234abcd/channel/1/releases").thenReply(200, JSON.stringify(data));
107+
108+
const result = await pollForAirgapReleaseStatus(apiClient, "1234abcd", "1", 0, "built");
109+
expect(result).toEqual("built");
110+
});
111+
});

src/channels.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getApplicationDetails } from "./applications";
22
import { VendorPortalApi } from "./configuration";
3+
import { Release } from "./releases";
34

45
export class Channel {
56
name: string;
@@ -9,6 +10,15 @@ export class Channel {
910
buildAirgapAutomatically?: boolean;
1011
}
1112

13+
export class StatusError extends Error {
14+
statusCode: number;
15+
16+
constructor(message: string, statusCode: number) {
17+
super(message);
18+
this.statusCode = statusCode;
19+
}
20+
}
21+
1222
export const exportedForTesting = {
1323
getChannelByApplicationId,
1424
findChannelDetailsInOutput
@@ -107,3 +117,56 @@ async function findChannelDetailsInOutput(channels: any[], { slug, name }: Chann
107117
}
108118
return Promise.reject({ channel: null, reason: `Could not find channel with slug ${slug} or name ${name}` });
109119
}
120+
121+
export async function pollForAirgapReleaseStatus(vendorPortalApi: VendorPortalApi, appId: string, channelId: string, releaseSequence: number, expectedStatus: string, timeout: number = 120, sleeptimeMs: number = 5000): Promise<string> {
122+
// get airgapped build release from the api, look for the status of the id to be ${status}
123+
// if it's not ${status}, sleep for 5 seconds and try again
124+
// if it is ${status}, return the release with that status
125+
// iterate for timeout/sleeptime times
126+
const iterations = (timeout * 1000) / sleeptimeMs;
127+
for (let i = 0; i < iterations; i++) {
128+
try {
129+
const release = await getAirgapBuildRelease(vendorPortalApi, appId, channelId, releaseSequence);
130+
if (release.airgapBuildStatus === expectedStatus) {
131+
return release.airgapBuildStatus;
132+
}
133+
if (release.airgapBuildStatus === "failed") {
134+
console.debug(`Airgapped build release ${releaseSequence} failed`);
135+
return "failed";
136+
}
137+
console.debug(`Airgapped build release ${releaseSequence} is not ready, sleeping for ${sleeptimeMs / 1000} seconds`);
138+
await new Promise(f => setTimeout(f, sleeptimeMs));
139+
} catch (err) {
140+
if (err instanceof StatusError) {
141+
if (err.statusCode >= 500) {
142+
// 5xx errors are likely transient, so we should retry
143+
console.debug(`Got HTTP error with status ${err.statusCode}, sleeping for ${sleeptimeMs / 1000} seconds`);
144+
await new Promise(f => setTimeout(f, sleeptimeMs));
145+
} else {
146+
console.debug(`Got HTTP error with status ${err.statusCode}, exiting`);
147+
throw err;
148+
}
149+
} else {
150+
throw err;
151+
}
152+
}
153+
}
154+
throw new Error(`Airgapped build release ${releaseSequence} did not reach status ${expectedStatus} in ${timeout} seconds`);
155+
}
156+
157+
async function getAirgapBuildRelease(vendorPortalApi: VendorPortalApi, appId: string, channelId: string, releaseSequence: number): Promise<Release> {
158+
const http = await vendorPortalApi.client();
159+
const uri = `${vendorPortalApi.endpoint}/app/${appId}/channel/${channelId}/releases`;
160+
const res = await http.get(uri);
161+
if (res.message.statusCode != 200) {
162+
// discard the response body
163+
await res.readBody();
164+
throw new Error(`Failed to get airgap build release: Server responded with ${res.message.statusCode}`);
165+
}
166+
const body: any = JSON.parse(await res.readBody());
167+
const release = body.releases.find((r: any) => r.sequence === releaseSequence);
168+
return {
169+
sequence: release.sequence,
170+
airgapBuildStatus: release.airgapBuildStatus
171+
};
172+
}

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export { VendorPortalApi } from "./configuration";
22
export { getApplicationDetails } from "./applications";
3-
export { Channel, createChannel, getChannelDetails, archiveChannel } from "./channels";
3+
export { Channel, createChannel, getChannelDetails, archiveChannel, pollForAirgapReleaseStatus } from "./channels";
44
export { ClusterVersion, createCluster, createClusterWithLicense, pollForStatus, getKubeconfig, removeCluster, upgradeCluster, getClusterVersions, createAddonObjectStore, pollForAddonStatus, exposeClusterPort } from "./clusters";
55
export { KubernetesDistribution, archiveCustomer, createCustomer, getUsedKubernetesDistributions } from "./customers";
6-
export { Release, CompatibilityResult, createRelease, createReleaseFromChart, promoteRelease, reportCompatibilityResult, pollForAirgapReleaseStatus } from "./releases";
6+
export { Release, CompatibilityResult, createRelease, createReleaseFromChart, promoteRelease, reportCompatibilityResult } from "./releases";

src/releases.spec.ts

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { VendorPortalApi } from "./configuration";
2-
import { ReleaseChart, exportedForTesting, KotsSingleSpec, createReleaseFromChart, Release, CompatibilityResult, pollForAirgapReleaseStatus } from "./releases";
2+
import { ReleaseChart, exportedForTesting, KotsSingleSpec, createReleaseFromChart, Release, CompatibilityResult } from "./releases";
33
import * as mockttp from "mockttp";
44
import * as fs from "fs-extra";
55
import * as path from "path";
@@ -298,30 +298,3 @@ describe("createReleaseFromChart", () => {
298298
expect(release.charts?.length).toEqual(1);
299299
});
300300
});
301-
302-
describe("pollForAirgapReleaseStatus", () => {
303-
const mockServer = mockttp.getLocal();
304-
const apiClient = new VendorPortalApi();
305-
apiClient.apiToken = "abcd1234";
306-
apiClient.endpoint = "http://localhost:8080";
307-
// Start your mock server
308-
beforeEach(() => {
309-
mockServer.start(8080);
310-
});
311-
afterEach(() => mockServer.stop());
312-
313-
it("poll for airgapped release status", async () => {
314-
const data = {
315-
releases: [
316-
{
317-
sequence: 0,
318-
airgapBuildStatus: "built"
319-
}
320-
]
321-
};
322-
await mockServer.forGet("/app/1234abcd/channel/1/releases").thenReply(200, JSON.stringify(data));
323-
324-
const result = await pollForAirgapReleaseStatus(apiClient, "1234abcd", "1", 0, "built");
325-
expect(result).toEqual("built");
326-
});
327-
});

src/releases.ts

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,6 @@ export const exportedForTesting = {
4646
reportCompatibilityResultByAppId
4747
};
4848

49-
export class StatusError extends Error {
50-
statusCode: number;
51-
52-
constructor(message: string, statusCode: number) {
53-
super(message);
54-
this.statusCode = statusCode;
55-
}
56-
}
57-
5849
export async function createRelease(vendorPortalApi: VendorPortalApi, appSlug: string, yamlDir: string): Promise<Release> {
5950
const http = await vendorPortalApi.client();
6051

@@ -351,56 +342,3 @@ async function reportCompatibilityResultByAppId(vendorPortalApi: VendorPortalApi
351342
// discard the response body
352343
await res.readBody();
353344
}
354-
355-
export async function pollForAirgapReleaseStatus(vendorPortalApi: VendorPortalApi, appId: string, channelId: string, releaseSequence: number, expectedStatus: string, timeout: number = 120, sleeptimeMs: number = 5000): Promise<string> {
356-
// get airgapped build release from the api, look for the status of the id to be ${status}
357-
// if it's not ${status}, sleep for 5 seconds and try again
358-
// if it is ${status}, return the release with that status
359-
// iterate for timeout/sleeptime times
360-
const iterations = (timeout * 1000) / sleeptimeMs;
361-
for (let i = 0; i < iterations; i++) {
362-
try {
363-
const release = await getAirgapBuildRelease(vendorPortalApi, appId, channelId, releaseSequence);
364-
if (release.airgapBuildStatus === expectedStatus) {
365-
return release.airgapBuildStatus;
366-
}
367-
if (release.airgapBuildStatus === "failed") {
368-
console.debug(`Airgapped build release ${releaseSequence} failed`);
369-
return "failed";
370-
}
371-
console.debug(`Airgapped build release ${releaseSequence} is not ready, sleeping for ${sleeptimeMs / 1000} seconds`);
372-
await new Promise(f => setTimeout(f, sleeptimeMs));
373-
} catch (err) {
374-
if (err instanceof StatusError) {
375-
if (err.statusCode >= 500) {
376-
// 5xx errors are likely transient, so we should retry
377-
console.debug(`Got HTTP error with status ${err.statusCode}, sleeping for ${sleeptimeMs / 1000} seconds`);
378-
await new Promise(f => setTimeout(f, sleeptimeMs));
379-
} else {
380-
console.debug(`Got HTTP error with status ${err.statusCode}, exiting`);
381-
throw err;
382-
}
383-
} else {
384-
throw err;
385-
}
386-
}
387-
}
388-
throw new Error(`Airgapped build release ${releaseSequence} did not reach status ${expectedStatus} in ${timeout} seconds`);
389-
}
390-
391-
async function getAirgapBuildRelease(vendorPortalApi: VendorPortalApi, appId: string, channelId: string, releaseSequence: number): Promise<Release> {
392-
const http = await vendorPortalApi.client();
393-
const uri = `${vendorPortalApi.endpoint}/app/${appId}/channel/${channelId}/releases`;
394-
const res = await http.get(uri);
395-
if (res.message.statusCode != 200) {
396-
// discard the response body
397-
await res.readBody();
398-
throw new Error(`Failed to get airgap build release: Server responded with ${res.message.statusCode}`);
399-
}
400-
const body: any = JSON.parse(await res.readBody());
401-
const release = body.releases.find((r: any) => r.sequence === releaseSequence);
402-
return {
403-
sequence: release.sequence,
404-
airgapBuildStatus: release.airgapBuildStatus
405-
};
406-
}

0 commit comments

Comments
 (0)