Skip to content

Commit

Permalink
Simplify short-lived token fetch
Browse files Browse the repository at this point in the history
- Always fetch a token for every hostname in the access key
- Use any tokens that are successfully fetched
- Retain access key if no tokens can be fetched
  • Loading branch information
bigdaz committed Jun 15, 2024
1 parent b532389 commit 16c6e4b
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 117 deletions.
11 changes: 1 addition & 10 deletions sources/src/develocity/build-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,7 @@ export async function setup(config: BuildScanConfig): Promise<void> {
maybeExportVariableNotEmpty('GRADLE_PLUGIN_REPOSITORY_USERNAME', config.getGradlePluginRepositoryUsername())
maybeExportVariableNotEmpty('GRADLE_PLUGIN_REPOSITORY_PASSWORD', config.getGradlePluginRepositoryPassword())

setupToken(
config.getDevelocityAccessKey(),
config.getDevelocityTokenExpiry(),
getEnv('DEVELOCITY_ENFORCE_URL'),
getEnv('DEVELOCITY_URL')
)
}

function getEnv(variableName: string): string | undefined {
return process.env[variableName]
setupToken(config.getDevelocityAccessKey(), config.getDevelocityTokenExpiry())
}

function maybeExportVariable(variableName: string, value: unknown): void {
Expand Down
70 changes: 11 additions & 59 deletions sources/src/develocity/short-lived-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,21 @@ import * as core from '@actions/core'
import {BuildScanConfig} from '../configuration'
import {recordDeprecation} from '../deprecation-collector'

export async function setupToken(
develocityAccessKey: string,
develocityTokenExpiry: string,
enforceUrl: string | undefined,
develocityUrl: string | undefined
): Promise<void> {
export async function setupToken(develocityAccessKey: string, develocityTokenExpiry: string): Promise<void> {
if (develocityAccessKey) {
try {
core.debug('Fetching short-lived token...')
const tokens = await getToken(enforceUrl, develocityUrl, develocityAccessKey, develocityTokenExpiry)
const tokens = await getToken(develocityAccessKey, develocityTokenExpiry)
if (tokens != null && !tokens.isEmpty()) {
core.debug(`Got token(s), setting the access key env vars`)
const token = tokens.raw()
core.setSecret(token)
exportAccessKeyEnvVars(token)
} else {
handleMissingAccessTokenWithDeprecationWarning()
handleMissingAccessToken()
}
} catch (e) {
handleMissingAccessTokenWithDeprecationWarning()
handleMissingAccessToken()
core.warning(`Failed to fetch short-lived token, reason: ${e}`)
}
}
Expand All @@ -34,7 +29,7 @@ function exportAccessKeyEnvVars(value: string): void {
)
}

function handleMissingAccessTokenWithDeprecationWarning(): void {
function handleMissingAccessToken(): void {
if (process.env[BuildScanConfig.GradleEnterpriseAccessKeyEnvVar]) {
// We do not clear the GRADLE_ENTERPRISE_ACCESS_KEY env var in v3, to let the users upgrade to DV 2024.1
recordDeprecation(`The ${BuildScanConfig.GradleEnterpriseAccessKeyEnvVar} env var is deprecated`)
Expand All @@ -44,50 +39,24 @@ function handleMissingAccessTokenWithDeprecationWarning(): void {
}
}

export async function getToken(
enforceUrl: string | undefined,
serverUrl: string | undefined,
accessKey: string,
expiry: string
): Promise<DevelocityAccessCredentials | null> {
export async function getToken(accessKey: string, expiry: string): Promise<DevelocityAccessCredentials | null> {
const empty: Promise<DevelocityAccessCredentials | null> = new Promise(r => r(null))
const develocityAccessKey = DevelocityAccessCredentials.parse(accessKey)
const shortLivedTokenClient = new ShortLivedTokenClient()

async function promiseError(message: string): Promise<DevelocityAccessCredentials | null> {
return new Promise((resolve, reject) => reject(new Error(message)))
}

if (develocityAccessKey == null) {
return empty
}
if (enforceUrl === 'true' || develocityAccessKey.isSingleKey()) {
if (!serverUrl) {
return promiseError('Develocity Server URL not configured')
}
const hostname = extractHostname(serverUrl)
if (hostname == null) {
return promiseError('Could not extract hostname from Develocity server URL')
}
const hostAccessKey = develocityAccessKey.forHostname(hostname)
if (!hostAccessKey) {
return promiseError(`Could not find corresponding key for hostname ${hostname}`)
}
try {
const token = await shortLivedTokenClient.fetchToken(serverUrl, hostAccessKey, expiry)
return DevelocityAccessCredentials.of([token])
} catch (e) {
return new Promise((resolve, reject) => reject(e))
}
}

const tokens = new Array<HostnameAccessKey>()
for (const k of develocityAccessKey.keys) {
try {
core.info(`Fetching short-lived Develocity access token for ${k.hostname}`)
const token = await shortLivedTokenClient.fetchToken(`https://${k.hostname}`, k, expiry)
tokens.push(token)
core.info(`Develocity short-lived access token obtained`)
} catch (e) {
// Ignoring failed token, TODO: log this ?
// Ignore failure to obtain token
core.info(`Failed to fetch Develocity access token: ${e}`)
}
}
if (tokens.length > 0) {
Expand All @@ -96,17 +65,8 @@ export async function getToken(
return empty
}

function extractHostname(serverUrl: string): string | null {
try {
const parsedUrl = new URL(serverUrl)
return parsedUrl.hostname
} catch (error) {
return null
}
}

class ShortLivedTokenClient {
httpc = new httpm.HttpClient('gradle/setup-gradle')
httpc = new httpm.HttpClient('gradle/actions/setup-gradle')
maxRetries = 3
retryInterval = 1000

Expand Down Expand Up @@ -187,14 +147,6 @@ export class DevelocityAccessCredentials {
return this.keys.length === 0
}

isSingleKey(): boolean {
return this.keys.length === 1
}

forHostname(hostname: string): HostnameAccessKey | undefined {
return this.keys.find(hostKey => hostKey.hostname === hostname)
}

raw(): string {
return this.keys
.map(k => `${k.hostname}${DevelocityAccessCredentials.hostDelimiter}${k.key}`)
Expand Down
76 changes: 28 additions & 48 deletions sources/test/jest/short-lived-token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,101 +37,81 @@ describe('short lived tokens', () => {
message: 'connect ECONNREFUSED 127.0.0.1:3333',
code: 'ECONNREFUSED'
})
try {
await getToken('true', 'http://localhost:3333', 'localhost=xyz;host1=key1', '')
expect('should have thrown').toBeUndefined()
} catch (e) {
// @ts-ignore
expect(e.code).toBe('ECONNREFUSED')
}
await expect(getToken('localhost=key0', ''))
.resolves
.toBeNull()
})

it('get short lived token fails when request fails', async () => {
it('get short lived token is null when request fails', async () => {
nock('http://dev:3333')
.post('/api/auth/token')
.times(3)
.reply(500, 'Internal error')
expect.assertions(1)
await expect(getToken('true', 'http://dev:3333', 'dev=xyz;host1=key1', ''))
.rejects
.toThrow('Develocity short lived token request failed http://dev:3333 with status code 500')
})

it('get short lived token fails when server url is not set', async () => {
expect.assertions(1)
await expect(getToken('true', undefined, 'localhost=xyz;host1=key1', ''))
.rejects
.toThrow('Develocity Server URL not configured')
await expect(getToken('dev=xyz', ''))
.resolves
.toBeNull()
})

it('get short lived token returns null when access key is empty', async () => {
expect.assertions(1)
await expect(getToken('true', 'http://dev:3333', '', ''))
await expect(getToken('', ''))
.resolves
.toBeNull()
})

it('get short lived token fails when host cannot be extracted from server url', async () => {
expect.assertions(1)
await expect(getToken('true', 'not_a_url', 'localhost=xyz;host1=key1', ''))
.rejects
.toThrow('Could not extract hostname from Develocity server URL')
})

it('get short lived token fails when access key does not contain corresponding host', async () => {
expect.assertions(1)
await expect(getToken('true', 'http://dev', 'host1=xyz;host2=key2', ''))
.rejects
.toThrow('Could not find corresponding key for hostname dev')
})

it('get short lived token succeeds when enforce url is true', async () => {
it('get short lived token succeeds when single key is set', async () => {
nock('https://dev')
.post('/api/auth/token')
.reply(200, 'token')
expect.assertions(1)
await expect(getToken('true', 'https://dev', 'dev=key1;host1=key2', ''))
await expect(getToken('dev=key1', ''))
.resolves
.toEqual({"keys": [{"hostname": "dev", "key": "token"}]})
})

it('get short lived token succeeds when enforce url is false and single key is set', async () => {
it('get short lived token succeeds when multiple keys are set', async () => {
nock('https://dev')
.post('/api/auth/token')
.reply(200, 'token')
.reply(200, 'token1')
nock('https://prod')
.post('/api/auth/token')
.reply(200, 'token2')
expect.assertions(1)
await expect(getToken('false', 'https://dev', 'dev=key1', ''))
await expect(getToken('dev=key1;prod=key2', ''))
.resolves
.toEqual({"keys": [{"hostname": "dev", "key": "token"}]})
.toEqual({"keys": [{"hostname": "dev", "key": "token1"}, {"hostname": "prod", "key": "token2"}]})
})

it('get short lived token succeeds when enforce url is false and multiple keys are set', async () => {
it('get short lived token succeeds when multiple keys are set and one is failing', async () => {
nock('https://dev')
.post('/api/auth/token')
.reply(200, 'token1')
nock('https://bogus')
.post('/api/auth/token')
.times(3)
.reply(500, 'Internal Error')
nock('https://prod')
.post('/api/auth/token')
.reply(200, 'token2')
expect.assertions(1)
await expect(getToken('false', 'https://dev', 'dev=key1;prod=key2', ''))
await expect(getToken('dev=key1;bogus=key0;prod=key2', ''))
.resolves
.toEqual({"keys": [{"hostname": "dev", "key": "token1"}, {"hostname": "prod", "key": "token2"}]})
})

it('get short lived token succeeds when enforce url is false and multiple keys are set and one is failing', async () => {
it('get short lived token is null when multiple keys are set and all are failing', async () => {
nock('https://dev')
.post('/api/auth/token')
.reply(200, 'token1')
.times(3)
.reply(500, 'Internal Error')
nock('https://bogus')
.post('/api/auth/token')
.times(3)
.reply(500, 'Internal Error')
nock('https://prod')
.post('/api/auth/token')
.reply(200, 'token2')
expect.assertions(1)
await expect(getToken('false', 'https://dev', 'dev=key1;bogus=key0;prod=key2', ''))
await expect(getToken('dev=key1;bogus=key0', ''))
.resolves
.toEqual({"keys": [{"hostname": "dev", "key": "token1"}, {"hostname": "prod", "key": "token2"}]})
.toBeNull()
})
})

0 comments on commit 16c6e4b

Please sign in to comment.