Skip to content

Commit d92f77c

Browse files
committed
refactor(core): migrate sts + ecr + ec2 + lambda
1 parent 999eaa9 commit d92f77c

File tree

7 files changed

+662
-103
lines changed

7 files changed

+662
-103
lines changed

package-lock.json

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

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@
593593
"@aws-sdk/client-docdb": "<3.731.0",
594594
"@aws-sdk/client-docdb-elastic": "<3.731.0",
595595
"@aws-sdk/client-ec2": "<3.731.0",
596+
"@aws-sdk/client-ecr": "~3.693.0",
596597
"@aws-sdk/client-ecs": "~3.693.0",
597598
"@aws-sdk/client-glue": "^3.852.0",
598599
"@aws-sdk/client-iam": "<3.731.0",

packages/core/src/shared/clients/ec2MetadataClient.ts

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
import { getLogger } from '../logger/logger'
77
import { ClassToInterfaceType } from '../utilities/tsUtils'
8-
import { MetadataService } from 'aws-sdk'
9-
import { ServiceException } from '@smithy/smithy-client'
8+
import { httpRequest } from '@smithy/credential-provider-imds'
9+
import { RequestOptions } from 'http'
1010

1111
export interface IamInfo {
1212
Code: string
@@ -22,8 +22,12 @@ export interface InstanceIdentity {
2222
export type Ec2MetadataClient = ClassToInterfaceType<DefaultEc2MetadataClient>
2323
export class DefaultEc2MetadataClient {
2424
private static readonly metadataServiceTimeout: number = 500
25+
// AWS EC2 Instance Metadata Service (IMDS) constants
26+
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-v2-how-it-works.html
27+
private static readonly metadataServiceHost: string = '169.254.169.254'
28+
private static readonly tokenPath: string = '/latest/api/token'
2529

26-
public constructor(private metadata: MetadataService = DefaultEc2MetadataClient.getMetadataService()) {}
30+
public constructor() {}
2731

2832
public getInstanceIdentity(): Promise<InstanceIdentity> {
2933
return this.invoke<InstanceIdentity>('/latest/dynamic/instance-identity/document')
@@ -33,52 +37,61 @@ export class DefaultEc2MetadataClient {
3337
return this.invoke<IamInfo>('/latest/meta-data/iam/info')
3438
}
3539

36-
public invoke<T>(path: string): Promise<T> {
37-
return new Promise((resolve, reject) => {
38-
// fetchMetadataToken is private for some reason, but has the exact token functionality
39-
// that we want out of the metadata service.
40-
// https://github.com/aws/aws-sdk-js/blob/3333f8b49283f5bbff823ab8a8469acedb7fe3d5/lib/metadata_service.js#L116-L136
41-
;(this.metadata as any).fetchMetadataToken((tokenErr: ServiceException, token: string) => {
42-
let options
43-
if (tokenErr) {
44-
getLogger().warn(
45-
'Ec2MetadataClient failed to fetch token. If this is an EC2 environment, then Toolkit will fall back to IMDSv1: %s',
46-
tokenErr
47-
)
40+
public async invoke<T>(path: string): Promise<T> {
41+
try {
42+
// Try to get IMDSv2 token first
43+
const token = await this.fetchMetadataToken()
44+
const headers: Record<string, string> = {}
45+
if (token) {
46+
headers['x-aws-ec2-metadata-token'] = token
47+
}
4848

49-
// Fall back to IMDSv1 for legacy instances.
50-
options = {}
51-
} else {
52-
options = {
53-
// By attaching the token we force the use of IMDSv2.
54-
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-v2-how-it-works.html
55-
headers: { 'x-aws-ec2-metadata-token': token },
56-
}
57-
}
49+
const response = await this.makeRequest(path, headers)
50+
return JSON.parse(response.toString())
51+
} catch (tokenErr) {
52+
getLogger().warn(
53+
'Ec2MetadataClient failed to fetch token. If this is an EC2 environment, then Toolkit will fall back to IMDSv1: %s',
54+
tokenErr
55+
)
5856

59-
this.metadata.request(path, options, (err, response) => {
60-
if (err) {
61-
reject(err)
62-
return
63-
}
64-
try {
65-
const jsonResponse: T = JSON.parse(response)
66-
resolve(jsonResponse)
67-
} catch (e) {
68-
reject(`Ec2MetadataClient: invalid response from "${path}": ${response}\nerror: ${e}`)
69-
}
70-
})
71-
})
72-
})
57+
// Fall back to IMDSv1 for legacy instances
58+
try {
59+
const response = await this.makeRequest(path, {})
60+
return JSON.parse(response.toString())
61+
} catch (err) {
62+
throw new Error(`Ec2MetadataClient: failed to fetch "${path}": ${err}`)
63+
}
64+
}
7365
}
7466

75-
private static getMetadataService() {
76-
return new MetadataService({
77-
httpOptions: {
67+
private async fetchMetadataToken(): Promise<string | undefined> {
68+
try {
69+
const options: RequestOptions = {
70+
host: DefaultEc2MetadataClient.metadataServiceHost,
71+
path: DefaultEc2MetadataClient.tokenPath,
72+
method: 'PUT',
73+
headers: {
74+
'x-aws-ec2-metadata-token-ttl-seconds': '21600',
75+
},
7876
timeout: DefaultEc2MetadataClient.metadataServiceTimeout,
79-
connectTimeout: DefaultEc2MetadataClient.metadataServiceTimeout,
80-
} as any,
81-
// workaround for known bug: https://github.com/aws/aws-sdk-js/issues/3029
82-
})
77+
}
78+
79+
const response = await httpRequest(options)
80+
return response.toString()
81+
} catch (err) {
82+
return undefined
83+
}
84+
}
85+
86+
private async makeRequest(path: string, headers: Record<string, string>): Promise<Buffer> {
87+
const options: RequestOptions = {
88+
host: DefaultEc2MetadataClient.metadataServiceHost,
89+
path,
90+
method: 'GET',
91+
headers,
92+
timeout: DefaultEc2MetadataClient.metadataServiceTimeout,
93+
}
94+
95+
return httpRequest(options)
8396
}
8497
}

packages/core/src/shared/clients/ecrClient.ts

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,31 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { ECR } from 'aws-sdk'
6+
import {
7+
ECRClient,
8+
DescribeImagesCommand,
9+
DescribeRepositoriesCommand,
10+
CreateRepositoryCommand,
11+
DeleteRepositoryCommand,
12+
BatchDeleteImageCommand,
13+
} from '@aws-sdk/client-ecr'
14+
import type { DescribeImagesRequest, DescribeRepositoriesRequest, Repository } from '@aws-sdk/client-ecr'
715
import globals from '../extensionGlobals'
816
import { AsyncCollection } from '../utilities/asyncCollection'
917
import { pageableToCollection } from '../utilities/collectionUtils'
1018
import { assertHasProps, ClassToInterfaceType, isNonNullable, RequiredProps } from '../utilities/tsUtils'
1119

12-
export type EcrRepository = RequiredProps<ECR.Repository, 'repositoryName' | 'repositoryUri' | 'repositoryArn'>
20+
export type EcrRepository = RequiredProps<Repository, 'repositoryName' | 'repositoryUri' | 'repositoryArn'>
1321

1422
export type EcrClient = ClassToInterfaceType<DefaultEcrClient>
1523
export class DefaultEcrClient {
1624
public constructor(public readonly regionCode: string) {}
1725

1826
public async *describeTags(repositoryName: string): AsyncIterableIterator<string> {
19-
const sdkClient = await this.createSdkClient()
20-
const request: ECR.DescribeImagesRequest = { repositoryName: repositoryName }
27+
const sdkClient = this.createSdkClient()
28+
const request: DescribeImagesRequest = { repositoryName: repositoryName }
2129
do {
22-
const response = await sdkClient.describeImages(request).promise()
30+
const response = await sdkClient.send(new DescribeImagesCommand(request))
2331
if (response.imageDetails) {
2432
for (const item of response.imageDetails) {
2533
if (item.imageTags !== undefined) {
@@ -34,13 +42,13 @@ export class DefaultEcrClient {
3442
}
3543

3644
public async *describeRepositories(): AsyncIterableIterator<EcrRepository> {
37-
const sdkClient = await this.createSdkClient()
38-
const request: ECR.DescribeRepositoriesRequest = {}
45+
const sdkClient = this.createSdkClient()
46+
const request: DescribeRepositoriesRequest = {}
3947
do {
40-
const response = await sdkClient.describeRepositories(request).promise()
48+
const response = await sdkClient.send(new DescribeRepositoriesCommand(request))
4149
if (response.repositories) {
4250
yield* response.repositories
43-
.map((repo) => {
51+
.map((repo: Repository) => {
4452
// If any of these are not present, the repo returned is not valid. repositoryUri/Arn
4553
// are both based on name, and it's not possible to not have a name
4654
if (!repo.repositoryArn || !repo.repositoryName || !repo.repositoryUri) {
@@ -53,36 +61,43 @@ export class DefaultEcrClient {
5361
}
5462
}
5563
})
56-
.filter((item) => item !== undefined) as EcrRepository[]
64+
.filter((item: EcrRepository | undefined) => item !== undefined) as EcrRepository[]
5765
}
5866
request.nextToken = response.nextToken
5967
} while (request.nextToken)
6068
}
6169

6270
public listAllRepositories(): AsyncCollection<EcrRepository[]> {
63-
const requester = async (req: ECR.DescribeRepositoriesRequest) =>
64-
(await this.createSdkClient()).describeRepositories(req).promise()
71+
const requester = async (req: DescribeRepositoriesRequest) =>
72+
this.createSdkClient().send(new DescribeRepositoriesCommand(req))
6573
const collection = pageableToCollection(requester, {}, 'nextToken', 'repositories')
6674

67-
return collection.filter(isNonNullable).map((list) => list.map((repo) => (assertHasProps(repo), repo)))
75+
return collection
76+
.filter(isNonNullable)
77+
.map((list: Repository[]) => list.map((repo: Repository) => (assertHasProps(repo), repo)))
6878
}
6979

7080
public async createRepository(repositoryName: string) {
71-
const sdkClient = await this.createSdkClient()
72-
return sdkClient.createRepository({ repositoryName: repositoryName }).promise()
81+
const sdkClient = this.createSdkClient()
82+
return sdkClient.send(new CreateRepositoryCommand({ repositoryName: repositoryName }))
7383
}
7484

7585
public async deleteRepository(repositoryName: string): Promise<void> {
76-
const sdkClient = await this.createSdkClient()
77-
await sdkClient.deleteRepository({ repositoryName: repositoryName }).promise()
86+
const sdkClient = this.createSdkClient()
87+
await sdkClient.send(new DeleteRepositoryCommand({ repositoryName: repositoryName }))
7888
}
7989

8090
public async deleteTag(repositoryName: string, tag: string): Promise<void> {
81-
const sdkClient = await this.createSdkClient()
82-
await sdkClient.batchDeleteImage({ repositoryName: repositoryName, imageIds: [{ imageTag: tag }] }).promise()
91+
const sdkClient = this.createSdkClient()
92+
await sdkClient.send(
93+
new BatchDeleteImageCommand({ repositoryName: repositoryName, imageIds: [{ imageTag: tag }] })
94+
)
8395
}
8496

85-
protected async createSdkClient(): Promise<ECR> {
86-
return await globals.sdkClientBuilder.createAwsService(ECR, undefined, this.regionCode)
97+
protected createSdkClient(): ECRClient {
98+
return globals.sdkClientBuilderV3.createAwsService({
99+
serviceClient: ECRClient,
100+
clientOptions: { region: this.regionCode },
101+
})
87102
}
88103
}

packages/core/src/shared/clients/lambdaClient.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import {
4040
import { CancellationError } from '../utilities/timeoutUtils'
4141
import { fromSSO } from '@aws-sdk/credential-provider-sso'
4242
import { getIAMConnection } from '../../auth/utils'
43-
import { WaiterConfiguration } from 'aws-sdk/lib/service'
4443
import { NodeHttpHandler } from '@smithy/node-http-handler'
4544

4645
export type LambdaClient = ClassToInterfaceType<DefaultLambdaClient>
@@ -301,11 +300,19 @@ export class DefaultLambdaClient {
301300
)
302301
}
303302

304-
public async waitForActive(functionName: string, waiter?: WaiterConfiguration): Promise<void> {
303+
public async waitForActive(
304+
functionName: string,
305+
waiter?: { maxWaitTime?: number; minDelay?: number; maxDelay?: number }
306+
): Promise<void> {
305307
const sdkClient = await this.createSdkClient()
306308

307309
await waitUntilFunctionActiveV2(
308-
{ client: sdkClient, maxWaitTime: (waiter?.maxAttempts ?? 600) * (waiter?.delay ?? 1) },
310+
{
311+
client: sdkClient,
312+
maxWaitTime: waiter?.maxWaitTime ?? 600,
313+
minDelay: waiter?.minDelay ?? 1,
314+
maxDelay: waiter?.maxDelay ?? 120,
315+
},
309316
{ FunctionName: functionName }
310317
)
311318
}

packages/core/src/shared/clients/stsClient.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,60 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { STS } from 'aws-sdk'
6+
import { STSClient, AssumeRoleCommand, GetCallerIdentityCommand } from '@aws-sdk/client-sts'
7+
import type { AssumeRoleRequest, AssumeRoleResponse, GetCallerIdentityResponse } from '@aws-sdk/client-sts'
8+
import { AwsCredentialIdentityProvider } from '@smithy/types'
79
import { Credentials } from '@aws-sdk/types'
810
import globals from '../extensionGlobals'
911
import { ClassToInterfaceType } from '../utilities/tsUtils'
1012

11-
export type GetCallerIdentityResponse = STS.GetCallerIdentityResponse
13+
export type { GetCallerIdentityResponse }
1214
export type StsClient = ClassToInterfaceType<DefaultStsClient>
15+
16+
// Helper function to convert Credentials to AwsCredentialIdentityProvider
17+
function toCredentialProvider(credentials: Credentials | AwsCredentialIdentityProvider): AwsCredentialIdentityProvider {
18+
if (typeof credentials === 'function') {
19+
return credentials
20+
}
21+
// Convert static credentials to provider function
22+
return async () => credentials
23+
}
24+
1325
export class DefaultStsClient {
1426
public constructor(
1527
public readonly regionCode: string,
16-
private readonly credentials?: Credentials,
28+
private readonly credentials?: Credentials | AwsCredentialIdentityProvider,
1729
private readonly endpointUrl?: string
1830
) {}
1931

20-
public async assumeRole(request: STS.AssumeRoleRequest): Promise<STS.AssumeRoleResponse> {
21-
const sdkClient = await this.createSdkClient()
22-
const response = await sdkClient.assumeRole(request).promise()
32+
public async assumeRole(request: AssumeRoleRequest): Promise<AssumeRoleResponse> {
33+
const sdkClient = this.createSdkClient()
34+
const response = await sdkClient.send(new AssumeRoleCommand(request))
2335
return response
2436
}
2537

26-
public async getCallerIdentity(): Promise<STS.GetCallerIdentityResponse> {
27-
const sdkClient = await this.createSdkClient()
28-
const response = await sdkClient.getCallerIdentity().promise()
38+
public async getCallerIdentity(): Promise<GetCallerIdentityResponse> {
39+
const sdkClient = this.createSdkClient()
40+
const response = await sdkClient.send(new GetCallerIdentityCommand({}))
2941
return response
3042
}
3143

32-
private async createSdkClient(): Promise<STS> {
33-
return await globals.sdkClientBuilder.createAwsService(
34-
STS,
35-
{
36-
credentials: this.credentials,
37-
stsRegionalEndpoints: 'regional',
38-
endpoint: this.endpointUrl,
39-
},
40-
this.regionCode
41-
)
44+
private createSdkClient(): STSClient {
45+
const clientOptions: { region: string; endpoint?: string; credentials?: AwsCredentialIdentityProvider } = {
46+
region: this.regionCode,
47+
}
48+
49+
if (this.endpointUrl) {
50+
clientOptions.endpoint = this.endpointUrl
51+
}
52+
53+
if (this.credentials) {
54+
clientOptions.credentials = toCredentialProvider(this.credentials)
55+
}
56+
57+
return globals.sdkClientBuilderV3.createAwsService({
58+
serviceClient: STSClient,
59+
clientOptions,
60+
})
4261
}
4362
}

0 commit comments

Comments
 (0)