Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | 3x 3x 8x 8x 8x 8x 8x 8x 8x 1x 8x 1x 8x 9x 9x 1x 1x 1x 9x 2x 2x 7x 7x 5x 3x 7x 7x 10x 2x 2x 7x 7x 10x 10x 10x 10x 5x 5x 5x 5x 5x 5x 10x 10x 10x 10x | import { channel as dc, tracingChannel } from 'node:diagnostics_channel'
import { loggers } from '@ydbjs/debug'
import { type RetryConfig, retry } from '@ydbjs/retry'
import { backoff } from '@ydbjs/retry/strategy'
import { CredentialsProvider } from './index.js'
let authTokenFetchCh = tracingChannel<{ provider: 'metadata' }, { provider: 'metadata' }>(
'tracing:ydb:auth.token.fetch'
)
let dbg = loggers.auth.extend('metadata')
export type MetadataCredentialsToken = {
value: string
expired_at: number
}
export type MetadataCredentials = {
endpoint?: string
flavor?: string
}
/**
* A credentials provider that retrieves tokens from a metadata service.
*
* This class extends the `CredentialsProvider` class and implements the `getToken` method
* to fetch tokens from a specified metadata endpoint. It supports optional retry logic
* and allows customization of the metadata flavor and endpoint.
*
* @extends CredentialsProvider
*/
export class MetadataCredentialsProvider extends CredentialsProvider {
#promise: Promise<string> | null = null
#token: MetadataCredentialsToken | null = null
#flavor: string = 'Google'
#endpoint: string =
'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token'
// See StaticCredentialsProvider — fire `token.expired` at most once per
// incident; reset on every successful refresh.
#expiredReported = false
/**
* Creates an instance of `MetadataCredentialsProvider`.
*
* @param credentials - An optional object containing metadata credentials.
* @param credentials.flavor - The metadata flavor (default: 'Google').
* @param credentials.endpoint - The metadata endpoint URL (default: 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token').
*/
constructor(credentials: MetadataCredentials = {}) {
super()
if (credentials.flavor) {
this.#flavor = credentials.flavor
}
if (credentials.endpoint) {
this.#endpoint = credentials.endpoint
}
dbg.log(
'creating metadata credentials provider with flavor: %s, endpoint: %s',
this.#flavor,
this.#endpoint
)
}
/**
* Retrieves an authentication token from the specified endpoint.
* If a valid token is already available and `force` is not true, it returns the cached token.
* Otherwise, it fetches a new token with optional retry logic based on the provided configuration.
*
* @param force - A flag indicating whether to force fetching a new token regardless of the existing one's validity.
* @param signal - An AbortSignal to cancel the operation if needed.
* @returns A promise resolving to the authentication token as a string.
* @throws Will throw an error if the token fetch fails, the response is not OK, or the content type is incorrect.
*/
async getToken(force?: boolean, signal?: AbortSignal): Promise<string> {
Iif (!force && this.#token && this.#token.expired_at > Date.now()) {
dbg.log('returning cached token, expires in %d ms', this.#token.expired_at - Date.now())
return this.#token.value
}
if (this.#token && this.#token.expired_at <= Date.now() && !this.#expiredReported) {
this.#expiredReported = true
let stalenessMs = Date.now() - this.#token.expired_at
dc('ydb:auth.token.expired').publish({ provider: 'metadata', stalenessMs })
}
if (this.#promise) {
dbg.log('token fetch already in progress, waiting for result')
return this.#promise
}
dbg.log('fetching new token from metadata service')
let retryConfig: RetryConfig = {
retry: (err) => err instanceof Error,
signal,
budget: 5,
strategy: backoff(10, 1000),
onRetry: (ctx) => {
dbg.log('retrying token fetch, attempt %d, error: %O', ctx.attempt, ctx.error)
},
}
this.#promise = authTokenFetchCh
.tracePromise(
() => retry(retryConfig, (attemptSignal) => this.#fetchTokenAttempt(attemptSignal)),
{ provider: 'metadata' }
)
.catch((error) => {
dc('ydb:auth.provider.failed').publish({ provider: 'metadata', error })
throw error
})
.finally(() => {
this.#promise = null
})
return this.#promise
}
async #fetchTokenAttempt(signal: AbortSignal): Promise<string> {
dbg.log('attempting to fetch token from %s', this.#endpoint)
let response = await fetch(this.#endpoint, {
headers: { 'Metadata-Flavor': this.#flavor },
signal,
})
dbg.log('%s %s %s', this.#endpoint, response.status, response.headers.get('Content-Type'))
if (!response.ok) {
let error = new Error(
`Failed to fetch token: ${response.status} ${response.statusText}`
)
dbg.log('error fetching token: %O', error)
throw error
}
let token = JSON.parse(await response.text()) as {
access_token?: string
expires_in?: number
}
Iif (!token.access_token) {
dbg.log('missing access token in response, response: %O', token)
throw new Error('No access token exists in response')
}
this.#token = {
value: token.access_token,
expired_at: Date.now() + (token.expires_in ?? 3600) * 1000,
}
dbg.log('token fetched successfully, expires in %d seconds', token.expires_in ?? 3600)
// Allow the next expiration incident to be reported.
this.#expiredReported = false
dc('ydb:auth.token.refreshed').publish({
provider: 'metadata',
expiresAt: this.#token.expired_at,
})
return this.#token.value
}
}
|