packages/@aws-cdk/toolkit-lib/lib/context-providers/load-balancers.ts (165 lines of code) (raw):
import type { LoadBalancerContextQuery, LoadBalancerListenerContextQuery } from '@aws-cdk/cloud-assembly-schema';
import type {
LoadBalancerContextResponse,
LoadBalancerListenerContextResponse,
} from '@aws-cdk/cx-api';
import {
LoadBalancerIpAddressType,
} from '@aws-cdk/cx-api';
import type { LoadBalancer, Listener, TagDescription } from '@aws-sdk/client-elastic-load-balancing-v2';
import type { IElasticLoadBalancingV2Client, SdkProvider } from '../api/aws-auth/private';
import { initContextProviderSdk } from '../api/aws-auth/private';
import type { ContextProviderPlugin } from '../api/plugin';
import { ContextProviderError } from '../api/toolkit-error';
/**
* Provides load balancer context information.
*/
export class LoadBalancerContextProviderPlugin implements ContextProviderPlugin {
constructor(private readonly aws: SdkProvider) {
}
async getValue(query: LoadBalancerContextQuery): Promise<LoadBalancerContextResponse> {
if (!query.loadBalancerArn && !query.loadBalancerTags) {
throw new ContextProviderError('The load balancer lookup query must specify either `loadBalancerArn` or `loadBalancerTags`');
}
const loadBalancer = await (await LoadBalancerProvider.getClient(this.aws, query)).getLoadBalancer();
const ipAddressType =
loadBalancer.IpAddressType === 'ipv4' ? LoadBalancerIpAddressType.IPV4 : LoadBalancerIpAddressType.DUAL_STACK;
return {
loadBalancerArn: loadBalancer.LoadBalancerArn!,
loadBalancerCanonicalHostedZoneId: loadBalancer.CanonicalHostedZoneId!,
loadBalancerDnsName: loadBalancer.DNSName!,
vpcId: loadBalancer.VpcId!,
securityGroupIds: loadBalancer.SecurityGroups ?? [],
ipAddressType: ipAddressType,
};
}
}
/**
* Provides load balancer listener context information
*/
export class LoadBalancerListenerContextProviderPlugin implements ContextProviderPlugin {
constructor(private readonly aws: SdkProvider) {
}
async getValue(query: LoadBalancerListenerContextQuery): Promise<LoadBalancerListenerContextResponse> {
if (!query.listenerArn && !query.loadBalancerArn && !query.loadBalancerTags) {
throw new ContextProviderError(
'The load balancer listener query must specify at least one of: `listenerArn`, `loadBalancerArn` or `loadBalancerTags`',
);
}
return (await LoadBalancerProvider.getClient(this.aws, query)).getListener();
}
}
class LoadBalancerProvider {
public static async getClient(
aws: SdkProvider,
query: LoadBalancerListenerContextQuery,
): Promise<LoadBalancerProvider> {
const client = (await initContextProviderSdk(aws, query)).elbv2();
try {
const listener = query.listenerArn
? // Assert we're sure there's at least one so it throws if not
(await client.describeListeners({ ListenerArns: [query.listenerArn] })).Listeners![0]!
: undefined;
return new LoadBalancerProvider(
client,
{ ...query, loadBalancerArn: listener?.LoadBalancerArn || query.loadBalancerArn },
listener,
);
} catch (err) {
throw new ContextProviderError(`No load balancer listeners found matching arn ${query.listenerArn}`);
}
}
constructor(
private readonly client: IElasticLoadBalancingV2Client,
private readonly filter: LoadBalancerListenerContextQuery,
private readonly listener?: Listener,
) {
}
public async getLoadBalancer(): Promise<LoadBalancer> {
const loadBalancers = await this.getLoadBalancers();
if (loadBalancers.length === 0) {
throw new ContextProviderError(`No load balancers found matching ${JSON.stringify(this.filter)}`);
}
if (loadBalancers.length > 1) {
throw new ContextProviderError(
`Multiple load balancers found matching ${JSON.stringify(this.filter)} - please provide more specific criteria`,
);
}
return loadBalancers[0];
}
public async getListener(): Promise<LoadBalancerListenerContextResponse> {
if (this.listener) {
try {
const loadBalancer = await this.getLoadBalancer();
return {
listenerArn: this.listener.ListenerArn!,
listenerPort: this.listener.Port!,
securityGroupIds: loadBalancer.SecurityGroups || [],
};
} catch (err) {
throw new ContextProviderError(`No associated load balancer found for listener arn ${this.filter.listenerArn}`);
}
}
const loadBalancers = await this.getLoadBalancers();
if (loadBalancers.length === 0) {
throw new ContextProviderError(
`No associated load balancers found for load balancer listener query ${JSON.stringify(this.filter)}`,
);
}
const listeners = (await this.getListenersForLoadBalancers(loadBalancers)).filter((listener) => {
return (
(!this.filter.listenerPort || listener.Port === this.filter.listenerPort) &&
(!this.filter.listenerProtocol || listener.Protocol === this.filter.listenerProtocol)
);
});
if (listeners.length === 0) {
throw new ContextProviderError(`No load balancer listeners found matching ${JSON.stringify(this.filter)}`);
}
if (listeners.length > 1) {
throw new ContextProviderError(
`Multiple load balancer listeners found matching ${JSON.stringify(this.filter)} - please provide more specific criteria`,
);
}
return {
listenerArn: listeners[0].ListenerArn!,
listenerPort: listeners[0].Port!,
securityGroupIds:
loadBalancers.find((lb) => listeners[0].LoadBalancerArn === lb.LoadBalancerArn)?.SecurityGroups || [],
};
}
private async getLoadBalancers() {
const loadBalancerArns = this.filter.loadBalancerArn ? [this.filter.loadBalancerArn] : undefined;
const loadBalancers = (
await this.client.paginateDescribeLoadBalancers({
LoadBalancerArns: loadBalancerArns,
})
).filter((lb) => lb.Type === this.filter.loadBalancerType);
return this.filterByTags(loadBalancers);
}
private async filterByTags(loadBalancers: LoadBalancer[]): Promise<LoadBalancer[]> {
if (!this.filter.loadBalancerTags) {
return loadBalancers;
}
return (await this.describeTags(loadBalancers.map((lb) => lb.LoadBalancerArn!)))
.filter((tagDescription) => {
// For every tag in the filter, there is some tag in the LB that matches it.
// In other words, the set of tags in the filter is a subset of the set of tags in the LB.
return this.filter.loadBalancerTags!.every((filter) => {
return tagDescription.Tags?.some((tag) =>
filter.key === tag.Key && filter.value === tag.Value);
});
})
.flatMap((tag) => loadBalancers.filter((loadBalancer) => tag.ResourceArn === loadBalancer.LoadBalancerArn));
}
/**
* Returns tag descriptions associated with the resources. The API doesn't support
* pagination, so this function breaks the resource list into chunks and issues
* the appropriate requests.
*/
private async describeTags(resourceArns: string[]): Promise<TagDescription[]> {
// Max of 20 resource arns per request.
const chunkSize = 20;
const tags = Array<TagDescription>();
for (let i = 0; i < resourceArns.length; i += chunkSize) {
const chunk = resourceArns.slice(i, Math.min(i + chunkSize, resourceArns.length));
const chunkTags = await this.client.describeTags({
ResourceArns: chunk,
});
tags.push(...(chunkTags.TagDescriptions || []));
}
return tags;
}
private async getListenersForLoadBalancers(loadBalancers: LoadBalancer[]): Promise<Listener[]> {
const listeners: Listener[] = [];
for (const loadBalancer of loadBalancers.map((lb) => lb.LoadBalancerArn)) {
listeners.push(...(await this.client.paginateDescribeListeners({ LoadBalancerArn: loadBalancer })));
}
return listeners;
}
}