Skip to content

Commit 75b3108

Browse files
authored
fix: Update service polling (#31)
* fix: Update service polling * lint * debug * Add timeout * Add comments * Throw error for ready timeouts * update test and merge * deep copy * debug * debug * debug * debug * remove print * Update unit test
1 parent 7d88f7a commit 75b3108

File tree

8 files changed

+153
-49
lines changed

8 files changed

+153
-49
lines changed

.github/workflows/deploy-cloudrun-credentials-it.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ name: deploy-cloudrun Credentials Integration
22

33
on:
44
push:
5-
branches-ignore:
6-
- 'example-*'
5+
branches:
6+
- 'main'
7+
pull_request:
78

89
jobs:
910
gcloud:

.github/workflows/deploy-cloudrun-it.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ name: deploy-cloudrun Integration
22

33
on:
44
push:
5-
branches-ignore:
6-
- 'example-*'
5+
branches:
6+
- 'main'
7+
pull_request:
78

89
jobs:
910
envvars:

.github/workflows/release-please.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
name: build and release-please
2+
13
on:
24
push:
35
branches:
46
- main
5-
name: build and release-please
7+
68
jobs:
79
build:
810
env:

src/cloudRun.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from 'google-auth-library';
2626
import { Service } from './service';
2727
import { MethodOptions } from 'googleapis-common';
28+
import { get } from 'lodash';
2829

2930
/**
3031
* Available options to create the client.
@@ -185,7 +186,7 @@ export class CloudRun {
185186
* @param service Service object.
186187
* @returns Service object.
187188
*/
188-
async deploy(service: Service): Promise<run_v1.Schema$Service> {
189+
async deploy(service: Service): Promise<string> {
189190
const authClient = await this.getAuthClient();
190191
const serviceNames = await this.listServices();
191192
let serviceResponse: GaxiosResponse<run_v1.Schema$Service>;
@@ -218,8 +219,11 @@ export class CloudRun {
218219
this.methodOptions,
219220
);
220221
}
222+
223+
const url = await this.pollService(serviceResponse.data);
221224
core.info(`Service ${service.name} has been successfully deployed.`);
222-
return serviceResponse.data;
225+
// return serviceResponse.data;
226+
return url;
223227
}
224228

225229
/**
@@ -299,4 +303,74 @@ export class CloudRun {
299303
this.methodOptions,
300304
);
301305
}
306+
307+
/**
308+
* Poll service revision until ready
309+
* @param serviceResponse
310+
* @returns service url or revision url
311+
*/
312+
async pollService(serviceResponse: run_v1.Schema$Service): Promise<string> {
313+
let url = getUrl(serviceResponse);
314+
const maxAttempts = 60; // Timeout after 300 seconds
315+
let attempt = 0;
316+
// Revision is ready and url is found before timeout
317+
while (!getReadyStatus(serviceResponse) && !url && attempt < maxAttempts) {
318+
attempt += 1;
319+
await sleep(5000);
320+
serviceResponse = await this.getService(serviceResponse.metadata!.name!);
321+
url = getUrl(serviceResponse);
322+
}
323+
if (!url) throw new Error('Timeout error: service revision is not ready.');
324+
return url;
325+
}
326+
}
327+
328+
/** Retrieve status of new revision */
329+
function getReadyStatus(serviceResponse: run_v1.Schema$Service): boolean {
330+
// Retrieve the revision name
331+
const revisionName = get(serviceResponse, 'spec.template.metadata.name');
332+
// Retrieve the revision statuses
333+
const createdRevision = get(
334+
serviceResponse,
335+
'status.latestCreatedRevisionName',
336+
);
337+
const latestRevision = get(serviceResponse, 'status.latestReadyRevisionName');
338+
// Latest created revision must equal latest ready revision
339+
if (revisionName) {
340+
// After first deployment, revision name is set
341+
return (
342+
revisionName &&
343+
createdRevision &&
344+
latestRevision &&
345+
revisionName == createdRevision &&
346+
revisionName == latestRevision
347+
);
348+
} else {
349+
// First deployment will not have a revision name
350+
return (
351+
createdRevision && latestRevision && latestRevision == createdRevision
352+
);
353+
}
354+
}
355+
356+
function sleep(ms: number): Promise<void> {
357+
return new Promise((resolve) => setTimeout(resolve, ms));
358+
}
359+
360+
/** Get service url or tagged revision url */
361+
function getUrl(serviceResponse: run_v1.Schema$Service): string {
362+
const revisionName = get(serviceResponse, 'spec.template.metadata.name');
363+
// Find revision url
364+
const traffic: run_v1.Schema$TrafficTarget[] = get(
365+
serviceResponse,
366+
'status.traffic',
367+
);
368+
let revision;
369+
if (traffic) {
370+
revision = traffic.find((revision) => {
371+
return revision.revisionName == revisionName && revision.url;
372+
});
373+
}
374+
// Or return service url
375+
return get(revision, 'url') || get(serviceResponse, 'status.url') || '';
302376
}

src/index.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@
1717
import * as core from '@actions/core';
1818
import { CloudRun } from './cloudRun';
1919
import { Service } from './service';
20-
import { get } from 'lodash';
21-
22-
function sleep(ms: number): Promise<void> {
23-
return new Promise((resolve) => setTimeout(resolve, ms));
24-
}
2520

2621
/**
2722
* Executes the main action. It includes the main business logic and is the
@@ -45,13 +40,10 @@ async function run(): Promise<void> {
4540
const service = new Service({ image, name, envVars, yaml });
4641

4742
// Deploy service
48-
let serviceResponse = await client.deploy(service);
49-
while (!get(serviceResponse, 'status.url')) {
50-
serviceResponse = await client.getService(service.name);
51-
await sleep(2000);
52-
}
43+
const url = await client.deploy(service);
44+
5345
// Set URL as output
54-
core.setOutput('url', get(serviceResponse, 'status.url'));
46+
core.setOutput('url', url);
5547
} catch (error) {
5648
core.setFailed(error.message);
5749
}

src/service.ts

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { run_v1 } from 'googleapis';
18-
import { get, merge } from 'lodash';
18+
import { get, merge, cloneDeep } from 'lodash';
1919
import fs from 'fs';
2020
import YAML from 'yaml';
2121

@@ -136,38 +136,49 @@ export class Service {
136136
* @param prevService the previous Cloud Run service revision
137137
*/
138138
public merge(prevService: run_v1.Schema$Service): void {
139+
const currentService = cloneDeep(this.request);
140+
139141
// Get Revision names if set
140-
const name = get(this.request, 'spec.template.metadata.name');
142+
const name = get(currentService, 'spec.template.metadata.name');
141143
const previousName = get(prevService, 'spec.template.metadata.name');
142-
143-
// Deep Merge Service
144-
const mergedServices = merge(prevService, this.request);
145-
146-
// Force update with Revision name change
147-
mergedServices.spec!.template!.metadata!.name = this.generateRevisionName(
148-
name,
149-
previousName,
150-
);
151-
152-
// Merge Container spec
153-
const prevEnvVars = prevService.spec!.template!.spec!.containers![0].env;
154-
const currentEnvVars = this.request.spec!.template!.spec!.containers![0]
155-
.env;
144+
const generatedName = this.generateRevisionName(name, previousName);
156145

157146
// Merge Env vars
147+
const prevEnvVars = get(prevService, 'spec.template.spec.containers')[0]
148+
.env;
149+
const currentEnvVars = get(
150+
currentService,
151+
'spec.template.spec.containers',
152+
)[0].env;
158153
let env: run_v1.Schema$EnvVar[] = [];
159154
if (currentEnvVars) {
160-
env = currentEnvVars.map((envVar) => envVar as run_v1.Schema$EnvVar);
155+
env = currentEnvVars.map(
156+
(envVar: run_v1.Schema$EnvVar) => envVar as run_v1.Schema$EnvVar,
157+
);
161158
}
162159
const keys = env?.map((envVar) => envVar.name);
163-
prevEnvVars?.forEach((envVar) => {
160+
prevEnvVars?.forEach((envVar: run_v1.Schema$EnvVar) => {
164161
if (!keys.includes(envVar.name)) {
165162
// Add old env vars without duplicating
166-
return env.push(envVar);
163+
env.push(envVar);
167164
}
168165
});
169-
// Set Env vars
170-
mergedServices.spec!.template!.spec!.containers![0].env = env;
166+
const newEnv = cloneDeep(env);
167+
168+
// Deep Merge Service
169+
const mergedServices = merge(prevService, currentService);
170+
171+
// Force update with Revision name change
172+
if (!get(mergedServices, 'spec.template.metadata')) {
173+
mergedServices.spec!.template!.metadata = {
174+
name: generatedName,
175+
};
176+
} else {
177+
mergedServices.spec!.template!.metadata!.name = generatedName;
178+
}
179+
180+
// Merge Container spec
181+
mergedServices.spec!.template!.spec!.containers![0].env = newEnv;
171182
this.request = mergedServices;
172183
}
173184

tests/unit/cloudRun.test.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ const image = 'gcr.io/cloudrun/hello';
2828
const name = `test-${Math.round(Math.random() * 100000)}`; // Cloud Run currently has name length restrictions
2929
const service = new Service({ image, name });
3030

31-
function sleep(ms: number): Promise<void> {
32-
return new Promise((resolve) => setTimeout(resolve, ms));
33-
}
34-
3531
describe('CloudRun', function() {
3632
it('initializes with JSON creds', function() {
3733
const client = new CloudRun(region, {
@@ -62,13 +58,13 @@ describe('CloudRun', function() {
6258
credentials: credentials,
6359
projectId: project,
6460
});
65-
let result = await client.deploy(service);
66-
while (!result.status!.url) {
67-
result = await client.getService(name);
68-
await sleep(2000);
61+
try {
62+
let result = await client.deploy(service);
63+
expect(result).to.include('run.app');
64+
} catch (err) {
65+
let result = await client.getService(service.name);
66+
expect(result).to.not.be.undefined;
6967
}
70-
expect(result).to.not.eql(null);
71-
expect(result.status!.url).to.include('run.app');
7268
await client.delete(service);
7369
});
7470
});

tests/unit/service.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,33 @@ describe('Service', function() {
6262
);
6363
});
6464

65+
it('retains env vars', function() {
66+
const envVars = 'KEY1=VALUE1';
67+
const service = new Service({ image, name, envVars });
68+
const containers = get(service, 'request.spec.template.spec.containers');
69+
const actual = containers[0]?.env[0];
70+
const expected: run_v1.Schema$EnvVar = {
71+
name: 'KEY1',
72+
value: 'VALUE1',
73+
};
74+
expect(actual.name).equal(expected.name);
75+
76+
const envVars2 = 'KEY2=VALUE2';
77+
const service2 = new Service({ image, name, envVars: envVars2 });
78+
service.merge(service2.request);
79+
const containers2 = get(service2, 'request.spec.template.spec.containers');
80+
const actual2 = containers2[0]?.env;
81+
expect(actual2.length).equal(2);
82+
const expected2: run_v1.Schema$EnvVar = {
83+
name: 'KEY2',
84+
value: 'VALUE2',
85+
};
86+
expect(actual2[1].name).equal(expected2.name);
87+
88+
const revisionName = get(service2, 'request.spec.template.metadata.name');
89+
expect(revisionName).to.not.be.undefined;
90+
});
91+
6592
it('parses yaml', function() {
6693
const yaml = './tests/unit/service.basic.yaml';
6794
const service = new Service({ image, name, yaml });

0 commit comments

Comments
 (0)