Skip to content

Commit b7a282c

Browse files
authored
Centralize request logic, turn on retries, and add debug logging (#323)
1 parent 9025e8f commit b7a282c

File tree

7 files changed

+294
-282
lines changed

7 files changed

+294
-282
lines changed

dist/index.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 137 additions & 156 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,18 @@
3131
},
3232
"devDependencies": {
3333
"@eslint/eslintrc": "^3.2.0",
34-
"@eslint/js": "^9.15.0",
35-
"@kubernetes/client-node": "^0.22.2",
36-
"@types/node": "^22.9.3",
37-
"@typescript-eslint/eslint-plugin": "^8.15.0",
38-
"@typescript-eslint/parser": "^8.15.0",
34+
"@eslint/js": "^9.17.0",
35+
"@kubernetes/client-node": "^0.22.3",
36+
"@types/node": "^22.10.2",
37+
"@typescript-eslint/eslint-plugin": "^8.18.0",
38+
"@typescript-eslint/parser": "^8.18.0",
3939
"@vercel/ncc": "^0.38.3",
4040
"eslint-config-prettier": "^9.1.0",
4141
"eslint-plugin-prettier": "^5.2.1",
42-
"eslint": "^9.15.0",
43-
"prettier": "^3.3.3",
42+
"eslint": "^9.17.0",
43+
"prettier": "^3.4.2",
4444
"ts-node": "^10.9.2",
45-
"typescript-eslint": "^8.15.0",
45+
"typescript-eslint": "^8.18.0",
4646
"typescript": "^5.7.2"
4747
}
4848
}

src/client.ts

Lines changed: 89 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { presence } from '@google-github-actions/actions-utils';
1818
import { GoogleAuth } from 'google-auth-library';
19-
import { Headers } from 'gaxios';
19+
import { Headers, GaxiosOptions } from 'gaxios';
2020
import YAML from 'yaml';
2121

2222
// Do not listen to the linter - this can NOT be rewritten as an ES6 import statement.
@@ -45,6 +45,16 @@ type ClientOptions = {
4545
projectID?: string;
4646
quotaProjectID?: string;
4747
location?: string;
48+
logger?: Logger;
49+
};
50+
51+
/**
52+
* Logger is the passed in logger on the client.
53+
*/
54+
type Logger = {
55+
debug: (message: string) => void; // eslint-disable-line no-unused-vars
56+
info: (message: string) => void; // eslint-disable-line no-unused-vars
57+
warn: (message: string) => void; // eslint-disable-line no-unused-vars
4858
};
4959

5060
/**
@@ -130,9 +140,11 @@ export class ClusterClient {
130140
* name is given (e.g. `c`), these values will be used to construct the full
131141
* resource name.
132142
*/
143+
readonly #logger?: Logger;
133144
readonly #projectID?: string;
134145
readonly #quotaProjectID?: string;
135146
readonly #location?: string;
147+
readonly #headers?: Headers;
136148

137149
readonly defaultEndpoint = 'https://container.googleapis.com/v1';
138150
readonly hubEndpoint = 'https://gkehub.googleapis.com/v1';
@@ -141,6 +153,8 @@ export class ClusterClient {
141153
readonly auth: GoogleAuth;
142154

143155
constructor(opts?: ClientOptions) {
156+
this.#logger = opts?.logger;
157+
144158
this.auth = new GoogleAuth({
145159
scopes: [
146160
'https://www.googleapis.com/auth/cloud-platform',
@@ -152,6 +166,13 @@ export class ClusterClient {
152166
this.#projectID = opts?.projectID;
153167
this.#quotaProjectID = opts?.quotaProjectID;
154168
this.#location = opts?.location;
169+
170+
this.#headers = {
171+
'User-Agent': userAgent,
172+
};
173+
if (this.#quotaProjectID) {
174+
this.#headers['X-Goog-User-Project'] = this.#quotaProjectID;
175+
}
155176
}
156177

157178
/**
@@ -160,13 +181,38 @@ export class ClusterClient {
160181
* @returns string
161182
*/
162183
async getToken(): Promise<string> {
184+
this.#logger?.debug(`Getting token`);
185+
163186
const token = await this.auth.getAccessToken();
164187
if (!token) {
165188
throw new Error('Failed to generate token.');
166189
}
167190
return token;
168191
}
169192

193+
/**
194+
* request is a wrapper around an authenticated request.
195+
*
196+
* @returns T
197+
*/
198+
async request<T>(opts: GaxiosOptions): Promise<T> {
199+
this.#logger?.debug(`Initiating request with options: ${JSON.stringify(opts)}`);
200+
201+
const mergedOpts: GaxiosOptions = {
202+
...{
203+
retry: true,
204+
headers: this.#headers,
205+
errorRedactor: false,
206+
},
207+
...opts,
208+
};
209+
210+
this.#logger?.debug(` Request options: ${JSON.stringify(mergedOpts)}`);
211+
212+
const resp = await this.auth.request<T>(mergedOpts);
213+
return resp.data;
214+
}
215+
170216
/**
171217
* Generates full resource name.
172218
*
@@ -209,14 +255,15 @@ export class ClusterClient {
209255
* @returns project number.
210256
*/
211257
async projectIDtoNum(projectID: string): Promise<string> {
258+
this.#logger?.debug(`Converting project ID '${projectID}' to a project number`);
259+
212260
const url = `${this.cloudResourceManagerEndpoint}/projects/${projectID}`;
213-
const resp = (await this.auth.request({
261+
const resp = await this.request<{ name: string }>({
214262
url: url,
215-
headers: this.#defaultHeaders(),
216-
})) as { data: { name: string } };
263+
});
217264

218265
// projectRef of form projects/<project-num>"
219-
const projectRef = resp.data?.name;
266+
const projectRef = resp.name;
220267
const projectNum = projectRef.replace('projects/', '');
221268
if (!projectRef.includes('projects/') || !projectNum) {
222269
throw new Error(
@@ -234,15 +281,15 @@ export class ClusterClient {
234281
* @returns endpoint.
235282
*/
236283
async getConnectGWEndpoint(name: string): Promise<string> {
284+
this.#logger?.debug(`Getting connect gateway endpoint for '${name}'`);
285+
237286
const membershipURL = `${this.hubEndpoint}/${name}`;
238-
const resp = (await this.auth.request({
287+
const membership = await this.request<HubMembershipResponse>({
239288
url: membershipURL,
240-
headers: this.#defaultHeaders(),
241-
})) as HubMembershipResponse;
289+
});
242290

243-
const membership = resp.data;
244291
if (!membership) {
245-
throw new Error(`Failed to lookup membership: ${resp}`);
292+
throw new Error(`Failed to lookup membership: ${name}`);
246293
}
247294

248295
// For GKE clusters, the configuration path is gkeMemberships, not
@@ -269,6 +316,8 @@ export class ClusterClient {
269316
* @returns Fleet membership name.
270317
*/
271318
async discoverClusterMembership(clusterName: string): Promise<string> {
319+
this.#logger?.debug(`Discovering cluster membership for '${clusterName}'`);
320+
272321
const clusterResourceLink = `//container.googleapis.com/${this.getResource(clusterName)}`;
273322
const projectID = this.#projectID;
274323
if (!projectID) {
@@ -278,12 +327,11 @@ export class ClusterClient {
278327
}
279328

280329
const url = `${this.hubEndpoint}/projects/${projectID}/locations/global/memberships?filter=endpoint.gkeCluster.resourceLink="${clusterResourceLink}"`;
281-
const resp = (await this.auth.request({
330+
const resp = await this.request<HubMembershipsResponse>({
282331
url: url,
283-
headers: this.#defaultHeaders(),
284-
})) as HubMembershipsResponse;
332+
});
285333

286-
const memberships = resp.data.resources;
334+
const memberships = resp.resources;
287335
if (!memberships || memberships.length < 1) {
288336
throw new Error(
289337
`Expected one membership for ${clusterName} in ${projectID}. ` +
@@ -309,11 +357,12 @@ export class ClusterClient {
309357
* @returns a Cluster object.
310358
*/
311359
async getCluster(clusterName: string): Promise<ClusterResponse> {
360+
this.#logger?.debug(`Getting information about cluster '${clusterName}'`);
361+
312362
const url = `${this.defaultEndpoint}/${this.getResource(clusterName)}`;
313-
const resp = (await this.auth.request({
363+
const resp = await this.request<ClusterResponse>({
314364
url: url,
315-
headers: this.#defaultHeaders(),
316-
})) as ClusterResponse;
365+
});
317366
return resp;
318367
}
319368

@@ -323,17 +372,19 @@ export class ClusterClient {
323372
* @param opts Input options. See CreateKubeConfigOptions.
324373
*/
325374
async createKubeConfig(opts: CreateKubeConfigOptions): Promise<string> {
375+
this.#logger?.debug(`Creating kubeconfig with options: ${JSON.stringify(opts)}`);
376+
326377
const cluster = opts.clusterData;
327-
let endpoint = cluster.data.endpoint;
378+
let endpoint = cluster.endpoint;
328379
const connectGatewayEndpoint = presence(opts.connectGWEndpoint);
329380
if (connectGatewayEndpoint) {
330381
endpoint = connectGatewayEndpoint;
331382
}
332383
if (opts.useInternalIP) {
333-
endpoint = cluster.data.privateClusterConfig.privateEndpoint;
384+
endpoint = cluster.privateClusterConfig.privateEndpoint;
334385
}
335386
if (opts.useDNSBasedEndpoint) {
336-
endpoint = cluster.data.controlPlaneEndpointsConfig.dnsEndpointConfig.endpoint;
387+
endpoint = cluster.controlPlaneEndpointsConfig.dnsEndpointConfig.endpoint;
337388
}
338389

339390
// By default, use the CA cert. Even if user doesn't specify
@@ -356,41 +407,30 @@ export class ClusterClient {
356407
{
357408
cluster: {
358409
...(useCACert && {
359-
'certificate-authority-data': cluster.data.masterAuth?.clusterCaCertificate,
410+
'certificate-authority-data': cluster.masterAuth?.clusterCaCertificate,
360411
}),
361412
server: `https://${endpoint}`,
362413
},
363-
name: cluster.data.name,
414+
name: cluster.name,
364415
},
365416
],
366417
'contexts': [
367418
{
368419
context: {
369-
cluster: cluster.data.name,
370-
user: cluster.data.name,
420+
cluster: cluster.name,
421+
user: cluster.name,
371422
namespace: opts.namespace,
372423
},
373424
name: contextName,
374425
},
375426
],
376427
'kind': 'Config',
377428
'current-context': contextName,
378-
'users': [{ ...{ name: cluster.data.name }, ...auth }],
429+
'users': [{ ...{ name: cluster.name }, ...auth }],
379430
};
380431

381432
return YAML.stringify(kubeConfig);
382433
}
383-
384-
#defaultHeaders(): Headers {
385-
const h: Headers = {
386-
'User-Agent': userAgent,
387-
};
388-
389-
if (this.#quotaProjectID) {
390-
h['X-Goog-User-Project'] = this.#quotaProjectID;
391-
}
392-
return h;
393-
}
394434
}
395435

396436
type cluster = {
@@ -454,19 +494,17 @@ export type KubeConfig = {
454494
};
455495

456496
export type ClusterResponse = {
457-
data: {
458-
name: string;
459-
endpoint: string;
460-
masterAuth: {
461-
clusterCaCertificate: string;
462-
};
463-
privateClusterConfig: {
464-
privateEndpoint: string;
465-
};
466-
controlPlaneEndpointsConfig: {
467-
dnsEndpointConfig: {
468-
endpoint: string;
469-
};
497+
name: string;
498+
endpoint: string;
499+
masterAuth: {
500+
clusterCaCertificate: string;
501+
};
502+
privateClusterConfig: {
503+
privateEndpoint: string;
504+
};
505+
controlPlaneEndpointsConfig: {
506+
dnsEndpointConfig: {
507+
endpoint: string;
470508
};
471509
};
472510
};
@@ -475,17 +513,13 @@ export type ClusterResponse = {
475513
* HubMembershipsResponse is the response from listing GKE Hub memberships.
476514
*/
477515
type HubMembershipsResponse = {
478-
data: {
479-
resources: HubMembership[];
480-
};
516+
resources: HubMembership[];
481517
};
482518

483519
/**
484520
* HubMembershipResponse is the response from getting a GKE Hub membership.
485521
*/
486-
type HubMembershipResponse = {
487-
data: HubMembership;
488-
};
522+
type HubMembershipResponse = HubMembership;
489523

490524
/**
491525
* HubMembership is a single HubMembership.

src/main.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
getInput,
2222
debug as logDebug,
2323
info as logInfo,
24+
warning as logWarning,
2425
setFailed,
2526
setOutput,
2627
} from '@actions/core';
@@ -105,6 +106,11 @@ export async function run(): Promise<void> {
105106
projectID: projectID,
106107
quotaProjectID: quotaProjectID,
107108
location: location,
109+
logger: {
110+
debug: logDebug,
111+
info: logInfo,
112+
warn: logWarning,
113+
},
108114
});
109115

110116
// Get Cluster object

0 commit comments

Comments
 (0)