diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 0adcc52901ba2..067575fb73ec0 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -149,15 +149,13 @@ const node = mesh.addVirtualNode('virtual-node', { }), listeners: [appmesh.VirtualNodeListener.httpNodeListener({ port: 8081, - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ healthyThreshold: 3, interval: Duration.seconds(5), // minimum path: `/health-check-path`, - port: 8080, - protocol: Protocol.HTTP, timeout: Duration.seconds(2), // minimum unhealthyThreshold: 2, - }, + }), })], accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), }); @@ -173,15 +171,13 @@ const node = new VirtualNode(this, 'node', { }), listeners: [appmesh.VirtualNodeListener.httpNodeListener({ port: 8080, - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ healthyThreshold: 3, interval: Duration.seconds(5), // min path: '/ping', - port: 8080, - protocol: Protocol.HTTP, timeout: Duration.seconds(2), // min unhealthyThreshold: 2, - }, + }), timeout: { idle: cdk.Duration.seconds(5), }, @@ -207,15 +203,13 @@ const node = new VirtualNode(this, 'node', { }), listeners: [appmesh.VirtualNodeListener.httpNodeListener({ port: 8080, - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ healthyThreshold: 3, interval: Duration.seconds(5), // min path: '/ping', - port: 8080, - protocol: Protocol.HTTP, timeout: Duration.seconds(2), // min unhealthyThreshold: 2, - }, + }), timeout: { idle: cdk.Duration.seconds(5), }, @@ -494,9 +488,9 @@ const gateway = new appmesh.VirtualGateway(stack, 'gateway', { mesh: mesh, listeners: [appmesh.VirtualGatewayListener.http({ port: 443, - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ interval: cdk.Duration.seconds(10), - }, + }), })], backendDefaults: { clientPolicy: appmesh.ClientPolicy.acmTrust({ @@ -517,9 +511,9 @@ const gateway = mesh.addVirtualGateway('gateway', { virtualGatewayName: 'virtualGateway', listeners: [appmesh.VirtualGatewayListener.http({ port: 443, - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ interval: cdk.Duration.seconds(10), - }, + }), })], }); ``` diff --git a/packages/@aws-cdk/aws-appmesh/lib/health-checks.ts b/packages/@aws-cdk/aws-appmesh/lib/health-checks.ts new file mode 100644 index 0000000000000..5ce022251acc6 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/health-checks.ts @@ -0,0 +1,189 @@ +import * as cdk from '@aws-cdk/core'; +import { CfnVirtualGateway, CfnVirtualNode } from './appmesh.generated'; +import { Protocol } from './shared-interfaces'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from 'constructs'; + +/** + * Properties used to define healthchecks. + */ +interface HealthCheckCommonOptions { + /** + * The number of consecutive successful health checks that must occur before declaring listener healthy. + * + * @default 2 + */ + readonly healthyThreshold?: number; + + /** + * The time period between each health check execution. + * + * @default Duration.seconds(5) + */ + readonly interval?: cdk.Duration; + + /** + * The amount of time to wait when receiving a response from the health check. + * + * @default Duration.seconds(2) + */ + readonly timeout?: cdk.Duration; + + /** + * The number of consecutive failed health checks that must occur before declaring a listener unhealthy. + * + * @default - 2 + */ + readonly unhealthyThreshold?: number; +} + +/** + * Properties used to define HTTP Based healthchecks. + */ +export interface HttpHealthCheckOptions extends HealthCheckCommonOptions { + /** + * The destination path for the health check request. + * + * @default / + */ + readonly path?: string; +} + +/** + * Properties used to define GRPC Based healthchecks. + */ +export interface GrpcHealthCheckOptions extends HealthCheckCommonOptions { } + +/** + * Properties used to define TCP Based healthchecks. + */ +export interface TcpHealthCheckOptions extends HealthCheckCommonOptions { } + +/** + * All Properties for Health Checks for mesh endpoints + */ +export interface HealthCheckConfig { + /** + * VirtualNode CFN configuration for Health Checks + * + * @default - no health checks + */ + readonly virtualNodeHealthCheck?: CfnVirtualNode.HealthCheckProperty; + + /** + * VirtualGateway CFN configuration for Health Checks + * + * @default - no health checks + */ + readonly virtualGatewayHealthCheck?: CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty; +} + +/** + * Options used for creating the Health Check object + */ +export interface HealthCheckBindOptions { + /** + * Port for Health Check interface + * + * @default - no default port is provided + */ + readonly defaultPort?: number; +} + + +/** + * Contains static factory methods for creating health checks for different protocols + */ +export abstract class HealthCheck { + /** + * Construct a HTTP health check + */ + public static http(options: HttpHealthCheckOptions = {}): HealthCheck { + return new HealthCheckImpl(Protocol.HTTP, options.healthyThreshold, options.unhealthyThreshold, options.interval, options.timeout, options.path); + } + + /** + * Construct a HTTP2 health check + */ + public static http2(options: HttpHealthCheckOptions = {}): HealthCheck { + return new HealthCheckImpl(Protocol.HTTP2, options.healthyThreshold, options.unhealthyThreshold, options.interval, options.timeout, options.path); + } + + /** + * Construct a GRPC health check + */ + public static grpc(options: GrpcHealthCheckOptions = {}): HealthCheck { + return new HealthCheckImpl(Protocol.GRPC, options.healthyThreshold, options.unhealthyThreshold, options.interval, options.timeout); + } + + /** + * Construct a TCP health check + */ + public static tcp(options: TcpHealthCheckOptions = {}): HealthCheck { + return new HealthCheckImpl(Protocol.TCP, options.healthyThreshold, options.unhealthyThreshold, options.interval, options.timeout); + } + + /** + * Called when the AccessLog type is initialized. Can be used to enforce + * mutual exclusivity with future properties + */ + public abstract bind(scope: Construct, options: HealthCheckBindOptions): HealthCheckConfig; +} + +class HealthCheckImpl extends HealthCheck { + constructor( + private readonly protocol: Protocol, + private readonly healthyThreshold: number = 2, + private readonly unhealthyThreshold: number = 2, + private readonly interval: cdk.Duration = cdk.Duration.seconds(5), + private readonly timeout: cdk.Duration = cdk.Duration.seconds(2), + private readonly path?: string) { + super(); + if (healthyThreshold < 2 || healthyThreshold > 10) { + throw new Error('healthyThreshold must be between 2 and 10'); + } + + if (unhealthyThreshold < 2 || unhealthyThreshold > 10) { + throw new Error('unhealthyThreshold must be between 2 and 10'); + } + + if (interval.toMilliseconds() < 5000 || interval.toMilliseconds() > 300_000) { + throw new Error('interval must be between 5 seconds and 300 seconds'); + } + + if (timeout.toMilliseconds() < 2000 || timeout.toMilliseconds() > 60_000) { + throw new Error('timeout must be between 2 seconds and 60 seconds'); + } + + // Default to / for HTTP Health Checks + if (path === undefined && (protocol === Protocol.HTTP || protocol === Protocol.HTTP2)) { + this.path = '/'; + } + } + + public bind(_scope: Construct, options: HealthCheckBindOptions): HealthCheckConfig { + return { + virtualNodeHealthCheck: { + protocol: this.protocol, + healthyThreshold: this.healthyThreshold, + unhealthyThreshold: this.unhealthyThreshold, + intervalMillis: this.interval.toMilliseconds(), + timeoutMillis: this.timeout.toMilliseconds(), + path: this.path, + port: options.defaultPort, + }, + virtualGatewayHealthCheck: { + protocol: this.protocol, + healthyThreshold: this.healthyThreshold, + unhealthyThreshold: this.unhealthyThreshold, + intervalMillis: this.interval.toMilliseconds(), + timeoutMillis: this.timeout.toMilliseconds(), + path: this.path, + port: options.defaultPort, + }, + }; + } + +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/index.ts b/packages/@aws-cdk/aws-appmesh/lib/index.ts index 1f5ca87def34d..4365a00da1279 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/index.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/index.ts @@ -16,3 +16,4 @@ export * from './virtual-gateway-listener'; export * from './gateway-route'; export * from './gateway-route-spec'; export * from './client-policy'; +export * from './health-checks'; diff --git a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts index 7b5cfe620de1c..8b6bd42f5b27e 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts @@ -1,45 +1,3 @@ -import * as cdk from '@aws-cdk/core'; -import { CfnVirtualGateway, CfnVirtualNode } from '../appmesh.generated'; - -type AppMeshHealthCheck = CfnVirtualNode.HealthCheckProperty | CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty - -/** - * Validates health check properties, throws an error if they are misconfigured. - * - * @param healthCheck Healthcheck property from a Virtual Node or Virtual Gateway - */ -export function validateHealthChecks(healthCheck: AppMeshHealthCheck) { - (Object.keys(healthCheck) as Array) - .filter((key) => - HEALTH_CHECK_PROPERTY_THRESHOLDS[key] && - typeof healthCheck[key] === 'number' && - !cdk.Token.isUnresolved(healthCheck[key]), - ).map((key) => { - const [min, max] = HEALTH_CHECK_PROPERTY_THRESHOLDS[key]!; - const value = healthCheck[key]!; - - if (value < min) { - throw new Error(`The value of '${key}' is below the minimum threshold (expected >=${min}, got ${value})`); - } - if (value > max) { - throw new Error(`The value of '${key}' is above the maximum threshold (expected <=${max}, got ${value})`); - } - }); -} - -/** - * Minimum and maximum thresholds for HeathCheck numeric properties - * - * @see https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HealthCheckPolicy.html - */ -const HEALTH_CHECK_PROPERTY_THRESHOLDS: {[key in (keyof AppMeshHealthCheck)]?: [number, number]} = { - healthyThreshold: [2, 10], - intervalMillis: [5000, 300000], - port: [1, 65535], - timeoutMillis: [2000, 60000], - unhealthyThreshold: [2, 10], -}; - /** * Generated Connection pool config */ diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index f3fc872bb6e41..31e4055c5baf0 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts @@ -1,7 +1,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnRoute } from './appmesh.generated'; -import { Protocol, HttpTimeout, GrpcTimeout, TcpTimeout } from './shared-interfaces'; +import { HttpTimeout, GrpcTimeout, Protocol, TcpTimeout } from './shared-interfaces'; import { IVirtualNode } from './virtual-node'; /** diff --git a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts index 61872d4b56bcd..513b7a8e553fa 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts @@ -56,6 +56,8 @@ export interface TcpTimeout { /** * Enum of supported AppMesh protocols + * + * @deprecated not for use outside package */ export enum Protocol { HTTP = 'http', @@ -64,63 +66,6 @@ export enum Protocol { GRPC = 'grpc', } -/** - * Properties used to define healthchecks when creating virtual nodes. - * All values have a default if only specified as {} when creating. - * If property not set, then no healthchecks will be defined. - */ -export interface HealthCheck { - /** - * Number of successful attempts before considering the node UP - * - * @default 2 - */ - readonly healthyThreshold?: number; - - /** - * Interval in milliseconds to re-check - * - * @default 5 seconds - */ - readonly interval?: cdk.Duration; - - /** - * The path where the application expects any health-checks, this can also be the application path. - * - * @default / - */ - readonly path?: string; - - /** - * The TCP port number for the healthcheck - * - * @default - same as corresponding port mapping - */ - readonly port?: number; - - /** - * The protocol to use for the healthcheck, for convinience a const enum has been defined. - * Protocol.HTTP or Protocol.TCP - * - * @default - same as corresponding port mapping - */ - readonly protocol?: Protocol; - - /** - * Timeout in milli-seconds for the healthcheck to be considered a fail. - * - * @default 2 seconds - */ - readonly timeout?: cdk.Duration; - - /** - * Number of failed attempts before considering the node DOWN. - * - * @default 2 - */ - readonly unhealthyThreshold?: number; -} - /** * Represents the outlier detection for a listener. */ diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway-listener.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway-listener.ts index cf429b388bb7a..6eb42c2c917e5 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway-listener.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway-listener.ts @@ -1,16 +1,16 @@ -import * as cdk from '@aws-cdk/core'; -import { Construct } from 'constructs'; import { CfnVirtualGateway } from './appmesh.generated'; -import { validateHealthChecks, ConnectionPoolConfig } from './private/utils'; +import { HealthCheck } from './health-checks'; +import { ConnectionPoolConfig } from './private/utils'; import { GrpcConnectionPool, - HealthCheck, Http2ConnectionPool, HttpConnectionPool, Protocol, } from './shared-interfaces'; import { TlsCertificate, TlsCertificateConfig } from './tls-certificate'; +import { Construct } from 'constructs'; + /** * Represents the properties needed to define a Listeners for a VirtualGateway */ @@ -140,7 +140,7 @@ class VirtualGatewayListenerImpl extends VirtualGatewayListener { port: this.port, protocol: this.protocol, }, - healthCheck: this.healthCheck ? renderHealthCheck(this.healthCheck, this.protocol, this.port): undefined, + healthCheck: this.healthCheck?.bind(scope, { defaultPort: this.port }).virtualGatewayHealthCheck, tls: tlsConfig ? renderTls(tlsConfig) : undefined, connectionPool: this.connectionPool ? renderConnectionPool(this.connectionPool, this.protocol) : undefined, }, @@ -159,34 +159,6 @@ function renderTls(tlsCertificateConfig: TlsCertificateConfig): CfnVirtualGatewa }; } -function renderHealthCheck(hc: HealthCheck, listenerProtocol: Protocol, - listenerPort: number): CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty { - - if (hc.protocol === Protocol.TCP) { - throw new Error('TCP health checks are not permitted for gateway listeners'); - } - - if (hc.protocol === Protocol.GRPC && hc.path) { - throw new Error('The path property cannot be set with Protocol.GRPC'); - } - - const protocol = hc.protocol? hc.protocol : listenerProtocol; - - const healthCheck: CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty = { - healthyThreshold: hc.healthyThreshold || 2, - intervalMillis: (hc.interval || cdk.Duration.seconds(5)).toMilliseconds(), // min - path: hc.path || ((protocol === Protocol.HTTP || protocol === Protocol.HTTP2) ? '/' : undefined), - port: hc.port || listenerPort, - protocol: hc.protocol || listenerProtocol, - timeoutMillis: (hc.timeout || cdk.Duration.seconds(2)).toMilliseconds(), - unhealthyThreshold: hc.unhealthyThreshold || 2, - }; - - validateHealthChecks(healthCheck); - - return healthCheck; -} - function renderConnectionPool(connectionPool: ConnectionPoolConfig, listenerProtocol: Protocol): CfnVirtualGateway.VirtualGatewayConnectionPoolProperty { return ({ diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts index f1606613950b6..67dd2bb8a3762 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts @@ -1,13 +1,14 @@ -import * as cdk from '@aws-cdk/core'; -import { Construct } from 'constructs'; import { CfnVirtualNode } from './appmesh.generated'; -import { validateHealthChecks, ConnectionPoolConfig } from './private/utils'; +import { HealthCheck } from './health-checks'; +import { ConnectionPoolConfig } from './private/utils'; import { - GrpcConnectionPool, GrpcTimeout, HealthCheck, Http2ConnectionPool, HttpConnectionPool, + GrpcConnectionPool, GrpcTimeout, Http2ConnectionPool, HttpConnectionPool, HttpTimeout, OutlierDetection, Protocol, TcpConnectionPool, TcpTimeout, } from './shared-interfaces'; import { TlsCertificate, TlsCertificateConfig } from './tls-certificate'; +import { Construct } from 'constructs'; + /** * Properties for a VirtualNode listener */ @@ -182,7 +183,7 @@ class VirtualNodeListenerImpl extends VirtualNodeListener { port: this.port, protocol: this.protocol, }, - healthCheck: this.healthCheck ? this.renderHealthCheck(this.healthCheck) : undefined, + healthCheck: this.healthCheck?.bind(scope, { defaultPort: this.port }).virtualNodeHealthCheck, timeout: this.timeout ? this.renderTimeout(this.timeout) : undefined, tls: tlsConfig ? this.renderTls(tlsConfig) : undefined, outlierDetection: this.outlierDetection ? this.renderOutlierDetection(this.outlierDetection) : undefined, @@ -201,32 +202,6 @@ class VirtualNodeListenerImpl extends VirtualNodeListener { }; } - private renderHealthCheck(hc: HealthCheck): CfnVirtualNode.HealthCheckProperty | undefined { - if (hc === undefined) { return undefined; } - - if (hc.protocol === Protocol.TCP && hc.path) { - throw new Error('The path property cannot be set with Protocol.TCP'); - } - - if (hc.protocol === Protocol.GRPC && hc.path) { - throw new Error('The path property cannot be set with Protocol.GRPC'); - } - - const healthCheck: CfnVirtualNode.HealthCheckProperty = { - healthyThreshold: hc.healthyThreshold || 2, - intervalMillis: (hc.interval || cdk.Duration.seconds(5)).toMilliseconds(), // min - path: hc.path || (hc.protocol === Protocol.HTTP ? '/' : undefined), - port: hc.port || this.port, - protocol: hc.protocol || this.protocol, - timeoutMillis: (hc.timeout || cdk.Duration.seconds(2)).toMilliseconds(), - unhealthyThreshold: hc.unhealthyThreshold || 2, - }; - - validateHealthChecks(healthCheck); - - return healthCheck; - } - private renderTimeout(timeout: HttpTimeout): CfnVirtualNode.ListenerTimeoutProperty { return ({ [this.protocol]: { @@ -267,4 +242,3 @@ class VirtualNodeListenerImpl extends VirtualNodeListener { }); } } - diff --git a/packages/@aws-cdk/aws-appmesh/package.json b/packages/@aws-cdk/aws-appmesh/package.json index 58873a1b8271f..56c6af1f11ad5 100644 --- a/packages/@aws-cdk/aws-appmesh/package.json +++ b/packages/@aws-cdk/aws-appmesh/package.json @@ -184,7 +184,8 @@ "duration-prop-type:@aws-cdk/aws-appmesh.TcpVirtualNodeListenerOptions.timeout", "duration-prop-type:@aws-cdk/aws-appmesh.GrpcRouteSpecOptions.timeout", "duration-prop-type:@aws-cdk/aws-appmesh.HttpRouteSpecOptions.timeout", - "duration-prop-type:@aws-cdk/aws-appmesh.TcpRouteSpecOptions.timeout" + "duration-prop-type:@aws-cdk/aws-appmesh.TcpRouteSpecOptions.timeout", + "no-unused-type:@aws-cdk/aws-appmesh.Protocol" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 68709def26f95..3e1b18a0073b1 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -31,10 +31,10 @@ const virtualService = new appmesh.VirtualService(stack, 'service', { const node = mesh.addVirtualNode('node', { serviceDiscovery: appmesh.ServiceDiscovery.dns(`node1.${namespace.namespaceName}`), listeners: [appmesh.VirtualNodeListener.http({ - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ healthyThreshold: 3, path: '/check-path', - }, + }), })], backends: [appmesh.Backend.virtualService(virtualService)], }); @@ -67,15 +67,13 @@ router.addRoute('route-1', { const node2 = mesh.addVirtualNode('node2', { serviceDiscovery: appmesh.ServiceDiscovery.dns(`node2.${namespace.namespaceName}`), listeners: [appmesh.VirtualNodeListener.http({ - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ healthyThreshold: 3, interval: cdk.Duration.seconds(5), path: '/check-path2', - port: 8080, - protocol: appmesh.Protocol.HTTP, timeout: cdk.Duration.seconds(2), unhealthyThreshold: 2, - }, + }), })], backendDefaults: { clientPolicy: appmesh.ClientPolicy.fileTrust({ @@ -93,15 +91,13 @@ const node2 = mesh.addVirtualNode('node2', { const node3 = mesh.addVirtualNode('node3', { serviceDiscovery: appmesh.ServiceDiscovery.dns(`node3.${namespace.namespaceName}`), listeners: [appmesh.VirtualNodeListener.http({ - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ healthyThreshold: 3, interval: cdk.Duration.seconds(5), path: '/check-path3', - port: 8080, - protocol: appmesh.Protocol.HTTP, timeout: cdk.Duration.seconds(2), unhealthyThreshold: 2, - }, + }), })], backendDefaults: { clientPolicy: appmesh.ClientPolicy.fileTrust({ @@ -208,9 +204,9 @@ new appmesh.VirtualGateway(stack, 'gateway2', { mesh: mesh, listeners: [appmesh.VirtualGatewayListener.http({ port: 443, - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ interval: cdk.Duration.seconds(10), - }, + }), tlsCertificate: appmesh.TlsCertificate.file({ certificateChainPath: 'path/to/certChain', privateKeyPath: 'path/to/privateKey', diff --git a/packages/@aws-cdk/aws-appmesh/test/test.health-check.ts b/packages/@aws-cdk/aws-appmesh/test/test.health-check.ts index 1ba7dc425da07..57c6aa9ee1d61 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.health-check.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.health-check.ts @@ -22,14 +22,14 @@ export = { // WHEN const toThrow = (millis: number) => getNode(stack).addListener(appmesh.VirtualNodeListener.http2({ - healthCheck: { interval: cdk.Duration.millis(millis) }, + healthCheck: appmesh.HealthCheck.http2({ interval: cdk.Duration.millis(millis) }), })); // THEN test.doesNotThrow(() => toThrow(min)); test.doesNotThrow(() => toThrow(max)); - test.throws(() => toThrow(min - 1), /below the minimum threshold/); - test.throws(() => toThrow(max + 1), /above the maximum threshold/); + test.throws(() => toThrow(min - 1), /interval must be between 5 seconds and 300 seconds/); + test.throws(() => toThrow(max + 1), /interval must be between 5 seconds and 300 seconds/); test.done(); }, @@ -41,32 +41,14 @@ export = { // WHEN const toThrow = (millis: number) => getNode(stack).addListener(appmesh.VirtualNodeListener.http2({ - healthCheck: { timeout: cdk.Duration.millis(millis) }, + healthCheck: appmesh.HealthCheck.http2({ timeout: cdk.Duration.millis(millis) }), })); // THEN test.doesNotThrow(() => toThrow(min)); test.doesNotThrow(() => toThrow(max)); - test.throws(() => toThrow(min - 1), /below the minimum threshold/); - test.throws(() => toThrow(max + 1), /above the maximum threshold/); - - test.done(); - }, - 'port'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const [min, max] = [1, 65535]; - - // WHEN - const toThrow = (port: number) => getNode(stack).addListener(appmesh.VirtualNodeListener.http({ - healthCheck: { port }, - })); - - // THEN - test.doesNotThrow(() => toThrow(min)); - test.doesNotThrow(() => toThrow(max)); - test.throws(() => toThrow(max + 1), /above the maximum threshold/); + test.throws(() => toThrow(min - 1), /timeout must be between 2 seconds and 60 seconds/); + test.throws(() => toThrow(max + 1), /timeout must be between 2 seconds and 60 seconds/); test.done(); }, @@ -78,14 +60,14 @@ export = { // WHEN const toThrow = (healthyThreshold: number) => getNode(stack).addListener(appmesh.VirtualNodeListener.http({ - healthCheck: { healthyThreshold }, + healthCheck: appmesh.HealthCheck.http({ healthyThreshold }), })); // THEN test.doesNotThrow(() => toThrow(min)); test.doesNotThrow(() => toThrow(max)); - test.throws(() => toThrow(min - 1), /below the minimum threshold/); - test.throws(() => toThrow(max + 1), /above the maximum threshold/); + test.throws(() => toThrow(min - 1), /healthyThreshold must be between 2 and 10/); + test.throws(() => toThrow(max + 1), /healthyThreshold must be between 2 and 10/); test.done(); }, @@ -97,53 +79,15 @@ export = { // WHEN const toThrow = (unhealthyThreshold: number) => getNode(stack).addListener(appmesh.VirtualNodeListener.http({ - healthCheck: { unhealthyThreshold }, + healthCheck: appmesh.HealthCheck.http({ unhealthyThreshold }), })); // THEN test.doesNotThrow(() => toThrow(min)); test.doesNotThrow(() => toThrow(max)); - test.throws(() => toThrow(min - 1), /below the minimum threshold/); - test.throws(() => toThrow(max + 1), /above the maximum threshold/); + test.throws(() => toThrow(min - 1), /unhealthyThreshold must be between 2 and 10/); + test.throws(() => toThrow(max + 1), /unhealthyThreshold must be between 2 and 10/); test.done(); }, - 'throws if path and Protocol.TCP'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const toThrow = (protocol: appmesh.Protocol) => getNode(stack).addListener(appmesh.VirtualNodeListener.http({ - healthCheck: { - protocol, - path: '/', - }, - })); - - // THEN - test.doesNotThrow(() => toThrow(appmesh.Protocol.HTTP)); - test.throws(() => toThrow(appmesh.Protocol.TCP), /The path property cannot be set with Protocol.TCP/); - - test.done(); - }, - - 'throws if path and Protocol.GRPC'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const toThrow = (protocol: appmesh.Protocol) => getNode(stack).addListener(appmesh.VirtualNodeListener.http({ - healthCheck: { - protocol, - path: '/', - }, - })); - - // THEN - test.doesNotThrow(() => toThrow(appmesh.Protocol.HTTP)); - test.throws(() => toThrow(appmesh.Protocol.GRPC), /The path property cannot be set with Protocol.GRPC/); - - test.done(); - }, - }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts index ac71a80017d0a..dfecbc4292c88 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts @@ -213,13 +213,13 @@ export = { serviceDiscovery: appmesh.ServiceDiscovery.dns('test.domain.local'), listeners: [appmesh.VirtualNodeListener.http({ port: 8080, - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ healthyThreshold: 3, path: '/', interval: cdk.Duration.seconds(5), // min timeout: cdk.Duration.seconds(2), // min unhealthyThreshold: 2, - }, + }), })], }); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts index 96d3c7cba9210..bafc0eccd6822 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts @@ -23,9 +23,9 @@ export = { mesh: mesh, listeners: [appmesh.VirtualGatewayListener.http({ port: 443, - healthCheck: { + healthCheck: appmesh.HealthCheck.http({ interval: cdk.Duration.seconds(10), - }, + }), })], }); @@ -33,9 +33,7 @@ export = { mesh: mesh, listeners: [appmesh.VirtualGatewayListener.http2({ port: 443, - healthCheck: { - interval: cdk.Duration.seconds(10), - }, + healthCheck: appmesh.HealthCheck.http2({ interval: cdk.Duration.seconds(10) }), })], }); @@ -115,8 +113,7 @@ export = { virtualGatewayName: 'test-gateway', listeners: [appmesh.VirtualGatewayListener.grpc({ port: 80, - healthCheck: { - }, + healthCheck: appmesh.HealthCheck.grpc(), })], mesh: mesh, accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts index f143b0025c1db..b993309547ab8 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts @@ -163,7 +163,7 @@ export = { serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), listeners: [appmesh.VirtualNodeListener.http2({ port: 80, - healthCheck: {}, + healthCheck: appmesh.HealthCheck.http2(), timeout: { idle: cdk.Duration.seconds(10) }, })], }); @@ -219,7 +219,9 @@ export = { node.addListener(appmesh.VirtualNodeListener.tcp({ port: 80, - healthCheck: { timeout: cdk.Duration.seconds(3) }, + healthCheck: appmesh.HealthCheck.tcp({ + timeout: cdk.Duration.seconds(3), + }), timeout: { idle: cdk.Duration.seconds(10) }, })); diff --git a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts index 5f3f5e30e5fad..7527fbf7c21ec 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts @@ -3,12 +3,10 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -// hack, as this is not exported by the Lambda module -import { calculateFunctionHash } from '@aws-cdk/aws-lambda/lib/function-hash'; import * as ssm from '@aws-cdk/aws-ssm'; import { - CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, - Resource, Stack, Stage, Token, + CfnResource, CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, + Lazy, Resource, Stack, Stage, Token, } from '@aws-cdk/core'; import { Construct, Node } from 'constructs'; @@ -156,17 +154,18 @@ export class EdgeFunction extends Resource implements lambda.IVersion { addEdgeLambdaToRoleTrustStatement(edgeFunction.role!); // Store the current version's ARN to be retrieved by the cross region reader below. + const version = edgeFunction.currentVersion; new ssm.StringParameter(edgeFunction, 'Parameter', { parameterName, - stringValue: edgeFunction.currentVersion.edgeArn, + stringValue: version.edgeArn, }); - const edgeArn = this.createCrossRegionArnReader(parameterNamePrefix, parameterName, edgeFunction); + const edgeArn = this.createCrossRegionArnReader(parameterNamePrefix, parameterName, version); return { edgeFunction, edgeArn }; } - private createCrossRegionArnReader(parameterNamePrefix: string, parameterName: string, edgeFunction: lambda.Function): string { + private createCrossRegionArnReader(parameterNamePrefix: string, parameterName: string, version: lambda.Version): string { // Prefix of the parameter ARN that applies to all EdgeFunctions. // This is necessary because the `CustomResourceProvider` is a singleton, and the `policyStatement` // must work for multiple EdgeFunctions. @@ -195,7 +194,15 @@ export class EdgeFunction extends Resource implements lambda.IVersion { Region: EdgeFunction.EDGE_REGION, ParameterName: parameterName, // This is used to determine when the function has changed, to refresh the ARN from the custom resource. - RefreshToken: calculateFunctionHash(edgeFunction), + // + // Use the logical id of the function version. Whenever a function version changes, the logical id must be + // changed for it to take effect - a good candidate for RefreshToken. + RefreshToken: Lazy.uncachedString({ + produce: () => { + const cfn = version.node.defaultChild as CfnResource; + return this.stack.resolve(cfn.logicalId); + }, + }), }, }); diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json index 6da7e8717d61f..d95e038c2c890 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json @@ -12,7 +12,7 @@ }, "Region": "us-east-1", "ParameterName": "/cdk/EdgeFunctionArn/eu-west-1/integ-distribution-lambda-cross-region/Lambda", - "RefreshToken": "4412ddb0ae449da20173ca211c51fddc" + "RefreshToken": "LambdaCurrentVersionDF706F6A97fb843e9bd06fcd2bb15eeace80e13e" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -138,7 +138,7 @@ }, "Region": "us-east-1", "ParameterName": "/cdk/EdgeFunctionArn/eu-west-1/integ-distribution-lambda-cross-region/Lambda2", - "RefreshToken": "8f81ceb404ac454f09648e62822d9ca9" + "RefreshToken": "Lambda2CurrentVersion72012B74b9eef8becb98501bc795baca3c6169c4" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/metric-types.ts b/packages/@aws-cdk/aws-cloudwatch/lib/metric-types.ts index eaa48446672f9..296400ee7f910 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/metric-types.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/metric-types.ts @@ -12,14 +12,14 @@ export interface IMetric { /** * Turn this metric object into an alarm configuration * - * @deprecated Use `toMetricsConfig()` instead. + * @deprecated Use `toMetricConfig()` instead. */ toAlarmConfig(): MetricAlarmConfig; /** * Turn this metric object into a graph configuration * - * @deprecated Use `toMetricsConfig()` instead. + * @deprecated Use `toMetricConfig()` instead. */ toGraphConfig(): MetricGraphConfig; } @@ -27,6 +27,8 @@ export interface IMetric { /** * Metric dimension * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cw-dimension.html + * */ export interface Dimension { /** diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts b/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts index bfb1e94dd19aa..f5a7340b90368 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/metric.ts @@ -212,7 +212,9 @@ export class Metric implements IMetric { if (periodSec !== 1 && periodSec !== 5 && periodSec !== 10 && periodSec !== 30 && periodSec % 60 !== 0) { throw new Error(`'period' must be 1, 5, 10, 30, or a multiple of 60 seconds, received ${periodSec}`); } - + if (props.dimensions) { + this.validateDimensions(props.dimensions); + } this.dimensions = props.dimensions; this.namespace = props.namespace; this.metricName = props.metricName; @@ -391,6 +393,26 @@ export class Metric implements IMetric { return list; } + + private validateDimensions(dims: DimensionHash): void { + var dimsArray = Object.keys(dims); + if (dimsArray?.length > 10) { + throw new Error(`The maximum number of dimensions is 10, received ${dimsArray.length}`); + } + + dimsArray.map(key => { + if (dims[key] === undefined || dims[key] === null) { + throw new Error(`Dimension value of '${dims[key]}' is invalid`); + }; + if (key.length < 1 || key.length > 255) { + throw new Error(`Dimension name must be at least 1 and no more than 255 characters; received ${key}`); + }; + + if (dims[key].length < 1 || dims[key].length > 255) { + throw new Error(`Dimension value must be at least 1 and no more than 255 characters; received ${dims[key]}`); + }; + }); + } } function asString(x?: unknown): string | undefined { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts index 18e1840e35cb6..e1dc9230e1404 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.metrics.ts @@ -51,4 +51,77 @@ export = { test.done(); }, + + 'cannot use null dimension value'(test: Test) { + test.throws(() => { + new Metric({ + namespace: 'Test', + metricName: 'ACount', + period: cdk.Duration.minutes(10), + dimensions: { + DimensionWithNull: null, + }, + }); + }, /Dimension value of 'null' is invalid/); + + test.done(); + }, + + 'cannot use undefined dimension value'(test: Test) { + test.throws(() => { + new Metric({ + namespace: 'Test', + metricName: 'ACount', + period: cdk.Duration.minutes(10), + dimensions: { + DimensionWithUndefined: undefined, + }, + }); + }, /Dimension value of 'undefined' is invalid/); + + test.done(); + }, + + 'cannot use long dimension values'(test: Test) { + const arr = new Array(256); + const invalidDimensionValue = arr.fill('A', 0).join(''); + + test.throws(() => { + new Metric({ + namespace: 'Test', + metricName: 'ACount', + period: cdk.Duration.minutes(10), + dimensions: { + DimensionWithLongValue: invalidDimensionValue, + }, + }); + }, `Dimension value must be at least 1 and no more than 255 characters; received ${invalidDimensionValue}`); + + test.done(); + }, + + 'throws error when there are more than 10 dimensions'(test: Test) { + test.throws(() => { + new Metric({ + namespace: 'Test', + metricName: 'ACount', + period: cdk.Duration.minutes(10), + dimensions: { + dimensionA: 'value1', + dimensionB: 'value2', + dimensionC: 'value3', + dimensionD: 'value4', + dimensionE: 'value5', + dimensionF: 'value6', + dimensionG: 'value7', + dimensionH: 'value8', + dimensionI: 'value9', + dimensionJ: 'value10', + dimensionK: 'value11', + }, + } ); + }, /The maximum number of dimensions is 10, received 11/); + + test.done(); + }, };