Skip to content

Commit

Permalink
fix: retry failed google-auth-library requests
Browse files Browse the repository at this point in the history
Adds a custom `gaxios` config to be used in `google-auth-library`
requests, setting a proper retry value. This should fix the flaky tests
on CI and provide a more resilient solution to end users.

Fix: #134
Fix: #146
  • Loading branch information
ruyadorno committed Aug 10, 2023
1 parent 96a28c7 commit 6d69f82
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 0 deletions.
31 changes: 31 additions & 0 deletions src/sqladmin-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import {GoogleAuth} from 'google-auth-library';
import {sqladmin_v1beta4} from '@googleapis/sqladmin';
import {GaxiosOptions, instance as gaxios} from 'gaxios';

Check failure on line 17 in src/sqladmin-fetcher.ts

View workflow job for this annotation

GitHub Actions / Lint

'GaxiosOptions' is defined but never used

Check failure on line 17 in src/sqladmin-fetcher.ts

View workflow job for this annotation

GitHub Actions / Lint

"gaxios" is extraneous
const {Sqladmin} = sqladmin_v1beta4;
import {InstanceConnectionInfo} from './instance-connection-info';
import {SslCert} from './ssl-cert';
Expand All @@ -33,6 +34,28 @@ interface RequestBody {
access_token?: string;
}


Check failure on line 37 in src/sqladmin-fetcher.ts

View workflow job for this annotation

GitHub Actions / Lint

Delete `⏎`
// https://github.com/googleapis/gaxios is the http request library used by
// google-auth-library and other Cloud SDK libraries, this function will set
// a standard default configuration that will ensure retry works as expected
// for internal google-auth-library requests.
function setupGaxiosConfig() {
gaxios.defaults = {
retryConfig: {
retry: 3,
httpMethodsToRetry: ['GET', 'HEAD', 'PUT', 'POST', 'OPTIONS', 'DELETE'],
noResponseRetries: 3,
},
};
}

const defaultGaxiosConfig = gaxios.defaults;
// resumes the previous default gaxios config in order to reduce the chance of
// affecting other libraries that might be sharing that same gaxios instance
function cleanGaxiosConfig() {
gaxios.defaults = defaultGaxiosConfig;
}

export class SQLAdminFetcher {
private readonly client: sqladmin_v1beta4.Sqladmin;
private readonly auth: GoogleAuth;
Expand Down Expand Up @@ -63,6 +86,8 @@ export class SQLAdminFetcher {
regionId,
instanceId,
}: InstanceConnectionInfo): Promise<InstanceMetadata> {
setupGaxiosConfig();

const res = await this.client.connect.get({
project: projectId,
instance: instanceId,
Expand Down Expand Up @@ -105,6 +130,8 @@ export class SQLAdminFetcher {
});
}

cleanGaxiosConfig();

return {
ipAddresses,
serverCaCert: {
Expand All @@ -119,6 +146,8 @@ export class SQLAdminFetcher {
publicKey: string,
authType: AuthTypes
): Promise<SslCert> {
setupGaxiosConfig();

const requestBody: RequestBody = {
public_key: publicKey,
};
Expand Down Expand Up @@ -172,6 +201,8 @@ export class SQLAdminFetcher {
tokenExpiration
);

cleanGaxiosConfig();

return {
cert,
expirationTime: nearestExpiration,
Expand Down
62 changes: 62 additions & 0 deletions test/sqladmin-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {resolve} from 'node:path';
import t from 'tap';
import nock from 'nock';
import {GoogleAuth} from 'google-auth-library';
Expand Down Expand Up @@ -381,3 +382,64 @@ t.test('getEphemeralCertificate sets access token', async t => {
'should return earlier token expiration time'
);
});

t.test('generateAccessToken endpoint should retry', async t => {
const path = t.testdir({
credentials: JSON.stringify({
delegates: [],
service_account_impersonation_url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/foo@dev.org:generateAccessToken',

Check failure on line 390 in test/sqladmin-fetcher.ts

View workflow job for this annotation

GitHub Actions / Lint

Insert `⏎·······`
source_credentials: {
client_id: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c',

Check failure on line 392 in test/sqladmin-fetcher.ts

View workflow job for this annotation

GitHub Actions / Lint

Insert `⏎·········`
client_secret: '7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730',

Check failure on line 393 in test/sqladmin-fetcher.ts

View workflow job for this annotation

GitHub Actions / Lint

Insert `⏎·········`
refresh_token: 'bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c',

Check failure on line 394 in test/sqladmin-fetcher.ts

View workflow job for this annotation

GitHub Actions / Lint

Insert `⏎·········`
type: 'authorized_user'

Check failure on line 395 in test/sqladmin-fetcher.ts

View workflow job for this annotation

GitHub Actions / Lint

Insert `,`
},
type: 'impersonated_service_account'

Check failure on line 397 in test/sqladmin-fetcher.ts

View workflow job for this annotation

GitHub Actions / Lint

Insert `,`
}),
});
const creds = process.env.GOOGLE_APPLICATION_CREDENTIALS;
process.env.GOOGLE_APPLICATION_CREDENTIALS = resolve(path, 'credentials');
t.teardown(() => {
process.env.GOOGLE_APPLICATION_CREDENTIALS = creds;
});

Check failure on line 404 in test/sqladmin-fetcher.ts

View workflow job for this annotation

GitHub Actions / Lint

Delete `⏎`


nock('https://oauth2.googleapis.com')
.persist()
.post('/token')
.reply(200, {access_token: 'abc123', expires_in: 1});

nock('https://oauth2.googleapis.com').post('/tokeninfo').reply(200, {
expires_in: 3600,
scope: 'https://www.googleapis.com/auth/sqlservice.login',
});

nock('https://iamcredentials.googleapis.com/v1')
.post('/projects/-/serviceAccounts/foo@dev.org:generateAccessToken')
.replyWithError({code: 'ECONNRESET'});

nock('https://iamcredentials.googleapis.com/v1')
.post('/projects/-/serviceAccounts/foo@dev.org:generateAccessToken')
.reply(200, {
accessToken: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c',
expireTime: new Date(Date.now() + 3600).toISOString()
});

const instanceConnectionInfo: InstanceConnectionInfo = {
projectId: 'my-project',
regionId: 'us-east1',
instanceId: 'my-instance',
};

// Second try should succeed
mockRequest(instanceConnectionInfo);

const fetcher = new SQLAdminFetcher();
const instanceMetadata = await fetcher.getInstanceMetadata(
instanceConnectionInfo
);
t.ok(
instanceMetadata,
'should return expected instance metadata object'
);
});

0 comments on commit 6d69f82

Please sign in to comment.