From 11873664b0c3235ddff7733971037cd26da8bdc3 Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Wed, 13 Mar 2019 02:29:22 -0700 Subject: [PATCH] feat(servicediscovery): AWS Cloud Map construct library (#1804) Add a construct library for AWS Cloud Map. Partially based on work by @jogold. --- .../@aws-cdk/aws-servicediscovery/README.md | 32 ++ .../lib/alias-target-instance.ts | 74 +++ .../lib/cname-instance.ts | 72 +++ .../lib/http-namespace.ts | 56 ++ .../aws-servicediscovery/lib/index.ts | 10 + .../aws-servicediscovery/lib/instance.ts | 55 ++ .../aws-servicediscovery/lib/ip-instance.ts | 120 +++++ .../aws-servicediscovery/lib/namespace.ts | 85 ++++ .../lib/non-ip-instance.ts | 58 +++ .../lib/private-dns-namespace.ts | 65 +++ .../lib/public-dns-namespace.ts | 56 ++ .../aws-servicediscovery/lib/service.ts | 418 +++++++++++++++ .../aws-servicediscovery/package.json | 24 +- ...ervice-with-cname-record.lit.expected.json | 52 ++ .../integ.service-with-cname-record.lit.ts | 21 + ...vice-with-http-namespace.lit.expected.json | 70 +++ .../integ.service-with-http-namespace.lit.ts | 31 ++ ...th-private-dns-namespace.lit.expected.json | 453 +++++++++++++++++ ....service-with-private-dns-namespace.lit.ts | 26 + ...ith-public-dns-namespace.lit.expected.json | 58 +++ ...g.service-with-public-dns-namespace.lit.ts | 27 + .../test/test.instance.ts | 477 ++++++++++++++++++ .../test/test.namespace.ts | 68 +++ .../aws-servicediscovery/test/test.service.ts | 453 +++++++++++++++++ .../test/test.servicediscovery.ts | 8 - 25 files changed, 2858 insertions(+), 11 deletions(-) create mode 100644 packages/@aws-cdk/aws-servicediscovery/lib/alias-target-instance.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/lib/cname-instance.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/lib/http-namespace.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/lib/instance.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/lib/ip-instance.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/lib/namespace.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/lib/non-ip-instance.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/lib/private-dns-namespace.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/lib/public-dns-namespace.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/lib/service.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-cname-record.lit.expected.json create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-cname-record.lit.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.lit.expected.json create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.lit.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.expected.json create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-public-dns-namespace.lit.expected.json create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-public-dns-namespace.lit.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/test.instance.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/test.namespace.ts create mode 100644 packages/@aws-cdk/aws-servicediscovery/test/test.service.ts delete mode 100644 packages/@aws-cdk/aws-servicediscovery/test/test.servicediscovery.ts diff --git a/packages/@aws-cdk/aws-servicediscovery/README.md b/packages/@aws-cdk/aws-servicediscovery/README.md index 82818175c1dc5..1c55c961fad01 100644 --- a/packages/@aws-cdk/aws-servicediscovery/README.md +++ b/packages/@aws-cdk/aws-servicediscovery/README.md @@ -1,2 +1,34 @@ ## The CDK Construct Library for AWS Service Discovery This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. + +This package contains constructs for working with **AWS Cloud Map** + +AWS Cloud Map is a fully managed service that you can use to create and +maintain a map of the backend services and resources that your applications +depend on. + +For further information on AWS Cloud Map, +see the [AWS Cloud Map documentation](https://docs.aws.amazon.com/cloud-map) + +The following example creates an AWS Cloud Map namespace that +supports API calls, creates a service in that namespace, and +registers an instance to it: + +[Creating a Cloud Map service within an HTTP namespace](test/integ.service-with-http-namespace.lit.ts) + +The following example creates an AWS Cloud Map namespace that +supports both API calls and DNS queries within a vpc, creates a +service in that namespace, and registers a loadbalancer as an +instance: + +[Creating a Cloud Map service within a Private DNS namespace](test/integ.service-with-private-dns-namespace.lit.ts) + +The following example creates an AWS Cloud Map namespace that +supports both API calls and public DNS queries, creates a service in +that namespace, and registers an IP instance: + +[Creating a Cloud Map service within a Public namespace](test/integ.service-with-public-dns-namespace.lit.ts) + +For DNS namespaces, you can also register instances to services with CNAME records: + +[Creating a Cloud Map service within a Public namespace](test/integ.service-with-cname-record.lit.ts) diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/alias-target-instance.ts b/packages/@aws-cdk/aws-servicediscovery/lib/alias-target-instance.ts new file mode 100644 index 0000000000000..2d5cafd623fe0 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/alias-target-instance.ts @@ -0,0 +1,74 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseInstanceProps, InstanceBase } from './instance'; +import { NamespaceType } from './namespace'; +import { DnsRecordType, IService, RoutingPolicy } from './service'; +import { CfnInstance } from './servicediscovery.generated'; + +/* + * Properties for an AliasTargetInstance + */ +export interface AliasTargetInstanceProps extends BaseInstanceProps { + /** + * DNS name of the target + */ + dnsName: string; + + /** + * The Cloudmap service this resource is registered to. + */ + service: IService; +} + +/* + * Instance that uses Route 53 Alias record type. Currently, the only resource types supported are Elastic Load + * Balancers. + */ +export class AliasTargetInstance extends InstanceBase { + /** + * The Id of the instance + */ + public readonly instanceId: string; + + /** + * The Cloudmap service to which the instance is registered. + */ + public readonly service: IService; + + /** + * The Route53 DNS name of the alias target + */ + public readonly dnsName: string; + + constructor(scope: cdk.Construct, id: string, props: AliasTargetInstanceProps) { + super(scope, id); + + if (props.service.namespace.type === NamespaceType.Http) { + throw new Error('Namespace associated with Service must be a DNS Namespace.'); + } + + // Should already be enforced when creating service, but validates if service is not instantiated with #createService + const dnsRecordType = props.service.dnsRecordType; + if (dnsRecordType !== DnsRecordType.A + && dnsRecordType !== DnsRecordType.AAAA + && dnsRecordType !== DnsRecordType.A_AAAA) { + throw new Error('Service must use `A` or `AAAA` records to register an AliasRecordTarget.'); + } + + if (props.service.routingPolicy !== RoutingPolicy.Weighted) { + throw new Error('Service must use `WEIGHTED` routing policy.'); + } + + const resource = new CfnInstance(this, 'Resource', { + instanceAttributes: { + AWS_ALIAS_DNS_NAME: props.dnsName, + ...props.customAttributes + }, + instanceId: props.instanceId || this.node.uniqueId, + serviceId: props.service.serviceId + }); + + this.service = props.service; + this.instanceId = resource.instanceId; + this.dnsName = props.dnsName; + } +} diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/cname-instance.ts b/packages/@aws-cdk/aws-servicediscovery/lib/cname-instance.ts new file mode 100644 index 0000000000000..c2df134d045fe --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/cname-instance.ts @@ -0,0 +1,72 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseInstanceProps, InstanceBase } from './instance'; +import { NamespaceType } from './namespace'; +import { DnsRecordType, IService } from './service'; +import { CfnInstance } from './servicediscovery.generated'; + +/* + * Properties for a CnameInstance used for service#registerCnameInstance + */ +export interface CnameInstanceBaseProps extends BaseInstanceProps { + /** + * If the service configuration includes a CNAME record, the domain name that you want Route 53 to + * return in response to DNS queries, for example, example.com. This value is required if the + * service specified by ServiceId includes settings for an CNAME record. + */ + instanceCname: string; +} + +/* + * Properties for a CnameInstance + */ +export interface CnameInstanceProps extends CnameInstanceBaseProps { + /** + * The Cloudmap service this resource is registered to. + */ + service: IService; +} + +/* + * Instance that is accessible using a domain name (CNAME). + */ +export class CnameInstance extends InstanceBase { + /** + * The Id of the instance + */ + public readonly instanceId: string; + + /** + * The Cloudmap service to which the instance is registered. + */ + public readonly service: IService; + + /** + * The domain name returned by DNS queries for the instance + */ + public readonly instanceCname: string; + + constructor(scope: cdk.Construct, id: string, props: CnameInstanceProps) { + super(scope, id); + + if (props.service.namespace.type === NamespaceType.Http) { + throw new Error('Namespace associated with Service must be a DNS Namespace.'); + } + + if (props.service.dnsRecordType !== DnsRecordType.CNAME) { + throw new Error('A `CnameIntance` can only be used with a service using a `CNAME` record.'); + } + + const resource = new CfnInstance(this, 'Resource', { + instanceId: props.instanceId || this.uniqueInstanceId(), + serviceId: props.service.serviceId, + instanceAttributes: { + AWS_INSTANCE_CNAME: props.instanceCname, + ...props.customAttributes + } + }); + + this.service = props.service; + this.instanceId = resource.instanceId; + this.instanceCname = props.instanceCname; + } +} diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/http-namespace.ts b/packages/@aws-cdk/aws-servicediscovery/lib/http-namespace.ts new file mode 100644 index 0000000000000..ea0564bd8bc34 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/http-namespace.ts @@ -0,0 +1,56 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseNamespaceProps, NamespaceBase, NamespaceType } from './namespace'; +import { BaseServiceProps, Service } from './service'; +import { CfnHttpNamespace } from './servicediscovery.generated'; + +// tslint:disable:no-empty-interface +export interface HttpNamespaceProps extends BaseNamespaceProps {} + +/** + * Define an HTTP Namespace + */ +export class HttpNamespace extends NamespaceBase { + /** + * A name for the namespace. + */ + public readonly namespaceName: string; + + /** + * Namespace Id for the namespace. + */ + public readonly namespaceId: string; + + /** + * Namespace Arn for the namespace. + */ + public readonly namespaceArn: string; + + /** + * Type of the namespace. + */ + public readonly type: NamespaceType; + + constructor(scope: cdk.Construct, id: string, props: HttpNamespaceProps) { + super(scope, id); + + const ns = new CfnHttpNamespace(this, 'Resource', { + name: props.name, + description: props.description + }); + + this.namespaceName = props.name; + this.namespaceId = ns.httpNamespaceId; + this.namespaceArn = ns.httpNamespaceArn; + this.type = NamespaceType.Http; + } + + /** + * Creates a service within the namespace + */ + public createService(id: string, props?: BaseServiceProps): Service { + return new Service(this, id, { + namespace: this, + ...props + }); + } +} diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/index.ts b/packages/@aws-cdk/aws-servicediscovery/lib/index.ts index 3683de632dddb..17cb4a95e5363 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/index.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/index.ts @@ -1,2 +1,12 @@ +export * from './instance'; +export * from './alias-target-instance'; +export * from './cname-instance'; +export * from './ip-instance'; +export * from './non-ip-instance'; +export * from './namespace'; +export * from './http-namespace'; +export * from './private-dns-namespace'; +export * from './public-dns-namespace'; +export * from './service'; // AWS::ServiceDiscovery CloudFormation Resources: export * from './servicediscovery.generated'; diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/instance.ts b/packages/@aws-cdk/aws-servicediscovery/lib/instance.ts new file mode 100644 index 0000000000000..0d064ed48760c --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/instance.ts @@ -0,0 +1,55 @@ +import cdk = require('@aws-cdk/cdk'); +import { IService } from './service'; + +export interface IInstance extends cdk.IConstruct { + /** + * The id of the instance resource + */ + readonly instanceId: string; + + /** + * The Cloudmap service this resource is registered to. + */ + readonly service: IService; +} + +/** + * Used when the resource that's associated with the service instance is accessible using values other than an IP + * address or a domain name (CNAME), i.e. for non-ip-instances + */ +export interface BaseInstanceProps { + /** + * The id of the instance resource + * + * @default Automatically generated name + */ + instanceId?: string; + + /** + * Custom attributes of the instance. + * + * @default none + */ + customAttributes?: { [key: string]: string }; +} + +export abstract class InstanceBase extends cdk.Construct implements IInstance { + /** + * The Id of the instance + */ + public abstract readonly instanceId: string; + + /** + * The Cloudmap service to which the instance is registered. + */ + public abstract readonly service: IService; + + /** + * Generate a unique instance Id that is safe to pass to CloudMap + */ + protected uniqueInstanceId() { + // Max length of 64 chars, get the last 64 chars + const id = this.node.uniqueId; + return id.substring(Math.max(id.length - 64, 0), id.length); + } +} diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/ip-instance.ts b/packages/@aws-cdk/aws-servicediscovery/lib/ip-instance.ts new file mode 100644 index 0000000000000..64600d73608d2 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/ip-instance.ts @@ -0,0 +1,120 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseInstanceProps, InstanceBase } from './instance'; +import { DnsRecordType, IService } from './service'; +import { CfnInstance } from './servicediscovery.generated'; + +/* + * Properties for a IpInstance used for service#registerIpInstance + */ +export interface IpInstanceBaseProps extends BaseInstanceProps { + /** + * The port on the endpoint that you want AWS Cloud Map to perform health checks on. This value is also used for + * the port value in an SRV record if the service that you specify includes an SRV record. You can also specify a + * default port that is applied to all instances in the Service configuration. + * + * @default 80 + */ + port?: number; + + /** + * If the service that you specify contains a template for an A record, the IPv4 address that you want AWS Cloud + * Map to use for the value of the A record. + * + * @default none + */ + ipv4?: string; + + /** + * If the service that you specify contains a template for an AAAA record, the IPv6 address that you want AWS Cloud + * Map to use for the value of the AAAA record. + * + * @default none + */ + ipv6?: string; +} + +/* + * Properties for an IpInstance + */ +export interface IpInstanceProps extends IpInstanceBaseProps { + /** + * The Cloudmap service this resource is registered to. + */ + service: IService; +} + +/* + * Instance that is accessible using an IP address. + */ +export class IpInstance extends InstanceBase { + /** + * The Id of the instance + */ + public readonly instanceId: string; + + /** + * The Cloudmap service to which the instance is registered. + */ + public readonly service: IService; + + /** + * The Ipv4 address of the instance, or blank string if none available + */ + public readonly ipv4: string; + + /** + * The Ipv6 address of the instance, or blank string if none available + */ + public readonly ipv6: string; + + /** + * The exposed port of the instance + */ + public readonly port: number; + + constructor(scope: cdk.Construct, id: string, props: IpInstanceProps) { + super(scope, id); + const dnsRecordType = props.service.dnsRecordType; + + if (dnsRecordType === DnsRecordType.CNAME) { + throw new Error('Service must support `A`, `AAAA` or `SRV` records to register this instance type.'); + } + if (dnsRecordType === DnsRecordType.SRV) { + if (!props.port) { + throw new Error('A `port` must be specified for a service using a `SRV` record.'); + } + + if (!props.ipv4 && !props.ipv6) { + throw new Error('At least `ipv4` or `ipv6` must be specified for a service using a `SRV` record.'); + } + } + + if (!props.ipv4 && (dnsRecordType === DnsRecordType.A || dnsRecordType === DnsRecordType.A_AAAA)) { + throw new Error('An `ipv4` must be specified for a service using a `A` record.'); + } + + if (!props.ipv6 && + (dnsRecordType === DnsRecordType.AAAA || dnsRecordType === DnsRecordType.A_AAAA)) { + throw new Error('An `ipv6` must be specified for a service using a `AAAA` record.'); + } + + const port = props.port || 80; + + const resource = new CfnInstance(this, 'Resource', { + instanceAttributes: { + AWS_INSTANCE_IPV4: props.ipv4, + AWS_INSTANCE_IPV6: props.ipv6, + AWS_INSTANCE_PORT: port.toString(), + ...props.customAttributes + }, + instanceId: props.instanceId || this.uniqueInstanceId(), + serviceId: props.service.serviceId + }); + + this.service = props.service; + this.instanceId = resource.instanceId; + this.ipv4 = props.ipv4 || ''; + this.ipv6 = props.ipv6 || ''; + this.port = port; + } +} diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/namespace.ts b/packages/@aws-cdk/aws-servicediscovery/lib/namespace.ts new file mode 100644 index 0000000000000..cdbeecc03ecf6 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/namespace.ts @@ -0,0 +1,85 @@ +import cdk = require('@aws-cdk/cdk'); + +export interface INamespace extends cdk.IConstruct { + /** + * A name for the Namespace. + */ + readonly namespaceName: string; + + /** + * Namespace Id for the Namespace. + */ + readonly namespaceId: string; + + /** + * Namespace ARN for the Namespace. + */ + readonly namespaceArn: string; + + /** + * Type of Namespace + */ + readonly type: NamespaceType; +} + +export interface BaseNamespaceProps { + /** + * A name for the Namespace. + */ + name: string; + + /** + * A description of the Namespace. + * + * @default none + */ + description?: string; +} + +export interface NamespaceImportProps { + /** + * A name for the Namespace. + */ + readonly namespaceName: string; + + /** + * Namespace Id for the Namespace. + */ + readonly namespaceId: string; + + /** + * Namespace ARN for the Namespace. + */ + readonly namespaceArn: string; + + /** + * Type of Namespace. Valid values: HTTP, DNS_PUBLIC, or DNS_PRIVATE + */ + readonly type: NamespaceType; +} + +export enum NamespaceType { + /** + * Choose this option if you want your application to use only API calls to discover registered instances. + */ + Http = "HTTP", + + /** + * Choose this option if you want your application to be able to discover instances using either API calls or using + * DNS queries in a VPC. + */ + DnsPrivate = "DNS_PRIVATE", + + /** + * Choose this option if you want your application to be able to discover instances using either API calls or using + * public DNS queries. You aren't required to use both methods. + */ + DnsPublic = "DNS_PUBLIC", +} + +export abstract class NamespaceBase extends cdk.Construct implements INamespace { + public abstract readonly namespaceId: string; + public abstract readonly namespaceArn: string; + public abstract readonly namespaceName: string; + public abstract readonly type: NamespaceType; +} diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/non-ip-instance.ts b/packages/@aws-cdk/aws-servicediscovery/lib/non-ip-instance.ts new file mode 100644 index 0000000000000..94c3c774daa6f --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/non-ip-instance.ts @@ -0,0 +1,58 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseInstanceProps, InstanceBase } from './instance'; +import { NamespaceType } from './namespace'; +import { IService } from './service'; +import { CfnInstance } from './servicediscovery.generated'; + +// tslint:disable-next-line:no-empty-interface +export interface NonIpInstanceBaseProps extends BaseInstanceProps { +} + +/* + * Properties for a NonIpInstance + */ +export interface NonIpInstanceProps extends NonIpInstanceBaseProps { + /** + * The Cloudmap service this resource is registered to. + */ + service: IService; +} + +/* + * Instance accessible using values other than an IP address or a domain name (CNAME). + * Specify the other values in Custom attributes. + */ +export class NonIpInstance extends InstanceBase { + /** + * The Id of the instance + */ + public readonly instanceId: string; + + /** + * The Cloudmap service to which the instance is registered. + */ + public readonly service: IService; + + constructor(scope: cdk.Construct, id: string, props: NonIpInstanceProps) { + super(scope, id); + + if (props.service.namespace.type !== NamespaceType.Http) { + throw new Error('This type of instance can only be registered for HTTP namespaces.'); + } + + if (props.customAttributes === undefined || Object.keys(props.customAttributes).length === 0) { + throw new Error('You must specify at least one custom attribute for this instance type.'); + } + + const resource = new CfnInstance(this, 'Resource', { + instanceId: props.instanceId || this.uniqueInstanceId(), + serviceId: props.service.serviceId, + instanceAttributes: { + ...props.customAttributes + } + }); + + this.service = props.service; + this.instanceId = resource.instanceId; + } +} diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/private-dns-namespace.ts b/packages/@aws-cdk/aws-servicediscovery/lib/private-dns-namespace.ts new file mode 100644 index 0000000000000..7fa2a662b3bf1 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/private-dns-namespace.ts @@ -0,0 +1,65 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { BaseNamespaceProps, NamespaceBase, NamespaceType } from './namespace'; +import { DnsServiceProps, Service } from './service'; +import { CfnPrivateDnsNamespace} from './servicediscovery.generated'; + +export interface PrivateDnsNamespaceProps extends BaseNamespaceProps { + /** + * The Amazon VPC that you want to associate the namespace with. + */ + vpc: ec2.IVpcNetwork; +} + +/** + * Define a Service Discovery HTTP Namespace + */ +export class PrivateDnsNamespace extends NamespaceBase { + /** + * The name of the PrivateDnsNamespace. + */ + public readonly namespaceName: string; + + /** + * Namespace Id of the PrivateDnsNamespace. + */ + public readonly namespaceId: string; + + /** + * Namespace Arn of the namespace. + */ + public readonly namespaceArn: string; + + /** + * Type of the namespace. + */ + public readonly type: NamespaceType; + + constructor(scope: cdk.Construct, id: string, props: PrivateDnsNamespaceProps) { + super(scope, id); + if (props.vpc === undefined) { + throw new Error(`VPC must be specified for PrivateDNSNamespaces`); + } + + const ns = new CfnPrivateDnsNamespace(this, 'Resource', { + name: props.name, + description: props.description, + vpc: props.vpc.vpcId + }); + + this.namespaceName = props.name; + this.namespaceId = ns.privateDnsNamespaceId; + this.namespaceArn = ns.privateDnsNamespaceArn; + this.type = NamespaceType.DnsPrivate; + } + + /** + * Creates a service within the namespace + */ + public createService(id: string, props?: DnsServiceProps): Service { + return new Service(this, id, { + namespace: this, + ...props + }); + } +} diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/public-dns-namespace.ts b/packages/@aws-cdk/aws-servicediscovery/lib/public-dns-namespace.ts new file mode 100644 index 0000000000000..a6c837c6e28c1 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/public-dns-namespace.ts @@ -0,0 +1,56 @@ +import cdk = require('@aws-cdk/cdk'); +import { BaseNamespaceProps, NamespaceBase, NamespaceType } from './namespace'; +import { DnsServiceProps, Service } from './service'; +import { CfnPublicDnsNamespace} from './servicediscovery.generated'; + +// tslint:disable:no-empty-interface +export interface PublicDnsNamespaceProps extends BaseNamespaceProps {} + +/** + * Define a Public DNS Namespace + */ +export class PublicDnsNamespace extends NamespaceBase { + /** + * A name for the namespace. + */ + public readonly namespaceName: string; + + /** + * Namespace Id for the namespace. + */ + public readonly namespaceId: string; + + /** + * Namespace Arn for the namespace. + */ + public readonly namespaceArn: string; + + /** + * Type of the namespace. + */ + public readonly type: NamespaceType; + + constructor(scope: cdk.Construct, id: string, props: PublicDnsNamespaceProps) { + super(scope, id); + + const ns = new CfnPublicDnsNamespace(this, 'Resource', { + name: props.name, + description: props.description, + }); + + this.namespaceName = props.name; + this.namespaceId = ns.publicDnsNamespaceId; + this.namespaceArn = ns.publicDnsNamespaceArn; + this.type = NamespaceType.DnsPublic; + } + + /** + * Creates a service within the namespace + */ + public createService(id: string, props?: DnsServiceProps): Service { + return new Service(this, id, { + namespace: this, + ...props + }); + } +} diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/service.ts b/packages/@aws-cdk/aws-servicediscovery/lib/service.ts new file mode 100644 index 0000000000000..6a640d71c7e4b --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/service.ts @@ -0,0 +1,418 @@ +import route53 = require('@aws-cdk/aws-route53'); +import cdk = require('@aws-cdk/cdk'); +import { AliasTargetInstance } from './alias-target-instance'; +import { CnameInstance, CnameInstanceBaseProps } from './cname-instance'; +import { IInstance } from './instance'; +import { IpInstance, IpInstanceBaseProps } from './ip-instance'; +import { INamespace, NamespaceType } from './namespace'; +import { NonIpInstance, NonIpInstanceBaseProps } from './non-ip-instance'; +import { CfnService } from './servicediscovery.generated'; + +export interface IService extends cdk.IConstruct { + /** + * A name for the Cloudmap Service. + */ + readonly serviceName: string; + + /** + * The namespace for the Cloudmap Service. + */ + readonly namespace: INamespace; + + /** + * The ID of the namespace that you want to use for DNS configuration. + */ + readonly serviceId: string; + + /** + * The Arn of the namespace that you want to use for DNS configuration. + */ + readonly serviceArn: string; + + /** + * The DnsRecordType used by the service + */ + readonly dnsRecordType: DnsRecordType; + + /** + * The Routing Policy used by the service + */ + readonly routingPolicy: RoutingPolicy; +} + +/** + * Basic props needed to create a service in a given namespace. Used by HttpNamespace.createService + */ +export interface BaseServiceProps { + /** + * A name for the Service. + * + * @default CloudFormation-generated name + */ + name?: string; + + /** + * A description of the service. + * + * @default none + */ + description?: string; + + /** + * Settings for an optional health check. If you specify health check settings, AWS Cloud Map associates the health + * check with the records that you specify in DnsConfig. Only one of healthCheckConfig or healthCheckCustomConfig can + * be specified. Not valid for PrivateDnsNamespaces. If you use healthCheck, you can only register IP instances to + * this service. + * + * @default none + */ + healthCheck?: HealthCheckConfig; + + /** + * Structure containing failure threshold for a custom health checker. + * Only one of healthCheckConfig or healthCheckCustomConfig can be specified. + * See: https://docs.aws.amazon.com/cloud-map/latest/api/API_HealthCheckCustomConfig.html + * + * @default none + */ + customHealthCheck?: HealthCheckCustomConfig; +} + +/** + * Service props needed to create a service in a given namespace. Used by createService() for PrivateDnsNamespace and + * PublicDnsNamespace + */ +export interface DnsServiceProps extends BaseServiceProps { + /** + * The DNS type of the record that you want AWS Cloud Map to create. Supported record types + * include A, AAAA, A and AAAA (A_AAAA), CNAME, and SRV. + * + * @default A + */ + dnsRecordType?: DnsRecordType; + + /** + * The amount of time, in seconds, that you want DNS resolvers to cache the settings for this + * record. + * + * @default 60 + */ + dnsTtlSec?: number; + + /** + * The routing policy that you want to apply to all DNS records that AWS Cloud Map creates when you + * register an instance and specify this service. + * + * @default WEIGHTED for CNAME records and when loadBalancer is true, MULTIVALUE otherwise + */ + routingPolicy?: RoutingPolicy; + + /** + * Whether or not this service will have an Elastic LoadBalancer registered to it as an AliasTargetInstance. + * + * Setting this to `true` correctly configures the `routingPolicy` + * and performs some additional validation. + * + * @default false + */ + loadBalancer?: boolean; +} + +export interface ServiceProps extends DnsServiceProps { + /** + * The ID of the namespace that you want to use for DNS configuration. + */ + namespace: INamespace; +} + +/** + * Define a CloudMap Service + */ +export class Service extends cdk.Construct implements IService { + /** + * A name for the Cloudmap Service. + */ + public readonly serviceName: string; + + /** + * The namespace for the Cloudmap Service. + */ + public readonly namespace: INamespace; + + /** + * The ID of the namespace that you want to use for DNS configuration. + */ + public readonly serviceId: string; + + /** + * The Arn of the namespace that you want to use for DNS configuration. + */ + public readonly serviceArn: string; + + /** + * The DnsRecordType used by the service + */ + public readonly dnsRecordType: DnsRecordType; + + /** + * The Routing Policy used by the service + */ + public readonly routingPolicy: RoutingPolicy; + + constructor(scope: cdk.Construct, id: string, props: ServiceProps) { + super(scope, id); + + const namespaceType = props.namespace.type; + + // Validations + if (namespaceType === NamespaceType.Http && (props.routingPolicy || props.dnsRecordType)) { + throw new Error('Cannot specify `routingPolicy` or `dnsRecord` when using an HTTP namespace.'); + } + + if (props.healthCheck && props.customHealthCheck) { + throw new Error('Cannot specify both `healthCheckConfig` and `healthCheckCustomConfig`.'); + } + + if (namespaceType === NamespaceType.DnsPrivate && props.healthCheck) { + throw new Error('Cannot specify `healthCheckConfig` for a Private DNS namespace.'); + } + + if (props.routingPolicy === RoutingPolicy.Multivalue + && props.dnsRecordType === DnsRecordType.CNAME) { + throw new Error('Cannot use `CNAME` record when routing policy is `Multivalue`.'); + } + + // Additional validation for eventual attachment of LBs + // The same validation happens later on during the actual attachment + // of LBs, but we need the property for the correct configuration of + // routingPolicy anyway, so might as well do the validation as well. + if (props.routingPolicy === RoutingPolicy.Multivalue + && props.loadBalancer) { + throw new Error('Cannot register loadbalancers when routing policy is `Multivalue`.'); + } + + if (props.healthCheck + && props.healthCheck.type === HealthCheckType.Tcp + && props.healthCheck.resourcePath) { + throw new Error('Cannot specify `resourcePath` when using a `TCP` health check.'); + } + + // Set defaults where necessary + const routingPolicy = (props.dnsRecordType === DnsRecordType.CNAME) || props.loadBalancer + ? RoutingPolicy.Weighted + : RoutingPolicy.Multivalue; + + const dnsRecordType = props.dnsRecordType || DnsRecordType.A; + + if (props.loadBalancer + && (!(dnsRecordType === DnsRecordType.A + || dnsRecordType === DnsRecordType.AAAA + || dnsRecordType === DnsRecordType.A_AAAA))) { + throw new Error('Must support `A` or `AAAA` records to register loadbalancers.'); + } + + const dnsConfig: CfnService.DnsConfigProperty | undefined = props.namespace.type === NamespaceType.Http + ? undefined + : { + dnsRecords: renderDnsRecords(dnsRecordType, props.dnsTtlSec), + namespaceId: props.namespace.namespaceId, + routingPolicy, + }; + + const healthCheckConfigDefaults = { + type: HealthCheckType.Http, + failureThreshold: 1, + resourcePath: props.healthCheck && props.healthCheck.type !== HealthCheckType.Tcp + ? '/' + : undefined + }; + + const healthCheckConfig = props.healthCheck && { ...healthCheckConfigDefaults, ...props.healthCheck }; + const healthCheckCustomConfig = props.customHealthCheck; + + // Create service + const service = new CfnService(this, 'Resource', { + name: props.name, + description: props.description, + dnsConfig, + healthCheckConfig, + healthCheckCustomConfig, + namespaceId: props.namespace.namespaceId + }); + + this.serviceName = service.serviceName; + this.serviceArn = service.serviceArn; + this.serviceId = service.serviceId; + this.namespace = props.namespace; + this.dnsRecordType = dnsRecordType; + this.routingPolicy = routingPolicy; + } + + /** + * Registers an ELB as a new instance with unique name instanceId in this service. + */ + public registerLoadBalancer(id: string, loadBalancer: route53.IAliasRecordTarget, customAttributes?: {[key: string]: string}): IInstance { + return new AliasTargetInstance(this, id, { + service: this, + dnsName: loadBalancer.asAliasRecordTarget().dnsName, + customAttributes + }); + } + + /** + * Registers a resource that is accessible using values other than an IP address or a domain name (CNAME). + */ + public registerNonIpInstance(props: NonIpInstanceBaseProps): IInstance { + return new NonIpInstance(this, "NonIpInstance", { + service: this, + instanceId: props.instanceId, + customAttributes: props.customAttributes + }); + } + + /** + * Registers a resource that is accessible using an IP address. + */ + public registerIpInstance(props: IpInstanceBaseProps): IInstance { + return new IpInstance(this, "IpInstance", { + service: this, + instanceId: props.instanceId, + ipv4: props.ipv4, + ipv6: props.ipv6, + port: props.port, + customAttributes: props.customAttributes + }); + } + + /** + * Registers a resource that is accessible using a CNAME. + */ + public registerCnameInstance(props: CnameInstanceBaseProps): IInstance { + return new CnameInstance(this, "CnameInstance", { + service: this, + instanceId: props.instanceId, + instanceCname: props.instanceCname, + customAttributes: props.customAttributes + }); + } +} + +function renderDnsRecords(dnsRecordType: DnsRecordType, dnsTtlSec?: number): CfnService.DnsRecordProperty[] { + const ttl = dnsTtlSec !== undefined ? dnsTtlSec.toString() : '60'; + + if (dnsRecordType === DnsRecordType.A_AAAA) { + return [{ + type: DnsRecordType.A, + ttl + }, { + type: DnsRecordType.AAAA, + ttl, + }]; + } else { + return [{ type: dnsRecordType, ttl }]; + } +} + +/** + * Settings for an optional Amazon Route 53 health check. If you specify settings for a health check, AWS Cloud Map + * associates the health check with all the records that you specify in DnsConfig. Only valid with a PublicDnsNamespace. + */ +export interface HealthCheckConfig { + /** + * The type of health check that you want to create, which indicates how Route 53 determines whether an endpoint is + * healthy. Cannot be modified once created. Supported values are HTTP, HTTPS, and TCP. + * + * @default HTTP + */ + type?: HealthCheckType; + + /** + * The path that you want Route 53 to request when performing health checks. Do not use when health check type is TCP. + * + * @default '/' + */ + resourcePath?: string; + + /** + * The number of consecutive health checks that an endpoint must pass or fail for Route 53 to change the current + * status of the endpoint from unhealthy to healthy or vice versa. + * + * @default 1 + */ + failureThreshold?: number; +} + +/** + * Specifies information about an optional custom health check. + */ +export interface HealthCheckCustomConfig { + /** + * The number of 30-second intervals that you want Cloud Map to wait after receiving an + * UpdateInstanceCustomHealthStatus request before it changes the health status of a service instance. + * + * @default 1 + */ + failureThreshold?: number; +} + +export enum DnsRecordType { + /** + * An A record + */ + A = "A", + + /** + * An AAAA record + */ + AAAA = "AAAA", + + /** + * Both an A and AAAA record + */ + A_AAAA = "A, AAAA", + + /** + * A Srv record + */ + SRV = "SRV", + + /** + * A CNAME record + */ + CNAME = "CNAME", +} + +export enum RoutingPolicy { + /** + * Route 53 returns the applicable value from one randomly selected instance from among the instances that you + * registered using the same service. + */ + Weighted = "WEIGHTED", + + /** + * If you define a health check for the service and the health check is healthy, Route 53 returns the applicable value + * for up to eight instances. + */ + Multivalue = "MULTIVALUE", +} + +export enum HealthCheckType { + /** + * Route 53 tries to establish a TCP connection. If successful, Route 53 submits an HTTP request and waits for an HTTP + * status code of 200 or greater and less than 400. + */ + Http = "HTTP", + + /** + * Route 53 tries to establish a TCP connection. If successful, Route 53 submits an HTTPS request and waits for an + * HTTP status code of 200 or greater and less than 400. If you specify HTTPS for the value of Type, the endpoint + * must support TLS v1.0 or later. + */ + Https = "HTTPS", + + /** + * Route 53 tries to establish a TCP connection. + * If you specify TCP for Type, don't specify a value for ResourcePath. + */ + Tcp = "TCP", +} diff --git a/packages/@aws-cdk/aws-servicediscovery/package.json b/packages/@aws-cdk/aws-servicediscovery/package.json index 6a2b328bdbc86..8c9efce160d4b 100644 --- a/packages/@aws-cdk/aws-servicediscovery/package.json +++ b/packages/@aws-cdk/aws-servicediscovery/package.json @@ -56,17 +56,35 @@ "devDependencies": { "@aws-cdk/assert": "^0.25.3", "cdk-build-tools": "^0.25.3", + "cdk-integ-tools": "^0.25.3", "cfn2ts": "^0.25.3", "pkglint": "^0.25.3" }, "dependencies": { - "@aws-cdk/cdk": "^0.25.3" + "@aws-cdk/cdk": "^0.25.3", + "@aws-cdk/aws-ec2": "^0.25.3", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.25.3", + "@aws-cdk/aws-route53": "^0.25.3" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { - "@aws-cdk/cdk": "^0.25.3" + "@aws-cdk/cdk": "^0.25.3", + "@aws-cdk/aws-ec2": "^0.25.3", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.25.3", + "@aws-cdk/aws-route53": "^0.25.3" }, "engines": { "node": ">= 8.10.0" + }, + "awslint": { + "exclude": [ + "export:@aws-cdk/aws-servicediscovery.IService", + "export:@aws-cdk/aws-servicediscovery.IInstance", + "import-props-interface:@aws-cdk/aws-servicediscovery.ServiceImportProps", + "import-props-interface:@aws-cdk/aws-servicediscovery.InstanceImportProps", + "resource-interface:@aws-cdk/aws-servicediscovery.IHttpNamespace", + "resource-interface:@aws-cdk/aws-servicediscovery.IPublicDnsNamespace", + "resource-interface:@aws-cdk/aws-servicediscovery.IPrivateDnsNamespace" + ] } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-cname-record.lit.expected.json b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-cname-record.lit.expected.json new file mode 100644 index 0000000000000..35fe96074827e --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-cname-record.lit.expected.json @@ -0,0 +1,52 @@ +{ + "Resources": { + "Namespace9B63B8C8": { + "Type": "AWS::ServiceDiscovery::PublicDnsNamespace", + "Properties": { + "Name": "foobar.com" + } + }, + "NamespaceServiceCABDF534": { + "Type": "AWS::ServiceDiscovery::Service", + "Properties": { + "DnsConfig": { + "DnsRecords": [ + { + "TTL": "30", + "Type": "CNAME" + } + ], + "NamespaceId": { + "Fn::GetAtt": [ + "Namespace9B63B8C8", + "Id" + ] + }, + "RoutingPolicy": "WEIGHTED" + }, + "Name": "foo", + "NamespaceId": { + "Fn::GetAtt": [ + "Namespace9B63B8C8", + "Id" + ] + } + } + }, + "NamespaceServiceCnameInstance5863ED3A": { + "Type": "AWS::ServiceDiscovery::Instance", + "Properties": { + "InstanceAttributes": { + "AWS_INSTANCE_CNAME": "service.pizza" + }, + "ServiceId": { + "Fn::GetAtt": [ + "NamespaceServiceCABDF534", + "Id" + ] + }, + "InstanceId": "awsservicediscoveryintegNamespaceServiceCnameInstance0F7DE989" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-cname-record.lit.ts b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-cname-record.lit.ts new file mode 100644 index 0000000000000..fb90f1964324c --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-cname-record.lit.ts @@ -0,0 +1,21 @@ +import cdk = require('@aws-cdk/cdk'); +import servicediscovery = require('../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-servicediscovery-integ'); + +const namespace = new servicediscovery.PublicDnsNamespace(stack, 'Namespace', { + name: 'foobar.com', +}); + +const service = namespace.createService('Service', { + name: 'foo', + dnsRecordType: servicediscovery.DnsRecordType.CNAME, + dnsTtlSec: 30 +}); + +service.registerCnameInstance({ + instanceCname: 'service.pizza', +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.lit.expected.json b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.lit.expected.json new file mode 100644 index 0000000000000..69355efb172d9 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.lit.expected.json @@ -0,0 +1,70 @@ +{ + "Resources": { + "MyNamespaceD0BB8558": { + "Type": "AWS::ServiceDiscovery::HttpNamespace", + "Properties": { + "Name": "covfefe" + } + }, + "MyNamespaceNonIpService3B425009": { + "Type": "AWS::ServiceDiscovery::Service", + "Properties": { + "Description": "service registering non-ip instances", + "NamespaceId": { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + } + } + }, + "MyNamespaceNonIpServiceNonIpInstanceFF8FB3DE": { + "Type": "AWS::ServiceDiscovery::Instance", + "Properties": { + "InstanceAttributes": { + "arn": "arn:aws:s3:::mybucket" + }, + "ServiceId": { + "Fn::GetAtt": [ + "MyNamespaceNonIpService3B425009", + "Id" + ] + }, + "InstanceId": "ervicediscoveryintegMyNamespaceNonIpServiceNonIpInstance45389A2A" + } + }, + "MyNamespaceIpService220F547F": { + "Type": "AWS::ServiceDiscovery::Service", + "Properties": { + "Description": "service registering ip instances", + "HealthCheckConfig": { + "FailureThreshold": 1, + "ResourcePath": "/check", + "Type": "HTTP" + }, + "NamespaceId": { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + } + } + }, + "MyNamespaceIpServiceIpInstance8CD1B210": { + "Type": "AWS::ServiceDiscovery::Instance", + "Properties": { + "InstanceAttributes": { + "AWS_INSTANCE_IPV4": "54.239.25.192", + "AWS_INSTANCE_PORT": "80" + }, + "ServiceId": { + "Fn::GetAtt": [ + "MyNamespaceIpService220F547F", + "Id" + ] + }, + "InstanceId": "awsservicediscoveryintegMyNamespaceIpServiceIpInstance56070746" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.lit.ts b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.lit.ts new file mode 100644 index 0000000000000..adb465fee7200 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.lit.ts @@ -0,0 +1,31 @@ +import cdk = require('@aws-cdk/cdk'); +import servicediscovery = require('../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-servicediscovery-integ'); + +const namespace = new servicediscovery.HttpNamespace(stack, 'MyNamespace', { + name: 'covfefe', +}); + +const service1 = namespace.createService('NonIpService', { + description: 'service registering non-ip instances', +}); + +service1.registerNonIpInstance({ + customAttributes: { arn: 'arn:aws:s3:::mybucket' } +}); + +const service2 = namespace.createService('IpService', { + description: 'service registering ip instances', + healthCheck: { + type: servicediscovery.HealthCheckType.Http, + resourcePath: '/check' + } +}); + +service2.registerIpInstance({ + ipv4: '54.239.25.192', +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.expected.json b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.expected.json new file mode 100644 index 0000000000000..73b030ceb8334 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.expected.json @@ -0,0 +1,453 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "Namespace9B63B8C8": { + "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace", + "Properties": { + "Name": "boobar.com", + "Vpc": { + "Ref": "Vpc8378EB38" + } + } + }, + "NamespaceServiceCABDF534": { + "Type": "AWS::ServiceDiscovery::Service", + "Properties": { + "DnsConfig": { + "DnsRecords": [ + { + "TTL": "30", + "Type": "A" + }, + { + "TTL": "30", + "Type": "AAAA" + } + ], + "NamespaceId": { + "Fn::GetAtt": [ + "Namespace9B63B8C8", + "Id" + ] + }, + "RoutingPolicy": "WEIGHTED" + }, + "NamespaceId": { + "Fn::GetAtt": [ + "Namespace9B63B8C8", + "Id" + ] + } + } + }, + "NamespaceServiceLoadbalancerD271391A": { + "Type": "AWS::ServiceDiscovery::Instance", + "Properties": { + "InstanceAttributes": { + "AWS_ALIAS_DNS_NAME": { + "Fn::GetAtt": [ + "LB8A12904C", + "DNSName" + ] + } + }, + "ServiceId": { + "Fn::GetAtt": [ + "NamespaceServiceCABDF534", + "Id" + ] + }, + "InstanceId": "awsservicediscoveryintegNamespaceServiceLoadbalancerA3D252B2" + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [], + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + ], + "Type": "application" + }, + "DependsOn": [ + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet2DefaultRoute97F91067" + ] + }, + "LBSecurityGroup8A41EA2B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB awsservicediscoveryintegLB0601B046", + "SecurityGroupEgress": [ + { + "CidrIp": "255.255.255.255/32", + "Description": "Disallow all traffic", + "FromPort": 252, + "IpProtocol": "icmp", + "ToPort": 86 + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.ts b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.ts new file mode 100644 index 0000000000000..657328517db17 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.ts @@ -0,0 +1,26 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import servicediscovery = require('../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-servicediscovery-integ'); + +const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); + +const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'Namespace', { + name: 'boobar.com', + vpc, +}); + +const service = namespace.createService('Service', { + dnsRecordType: servicediscovery.DnsRecordType.A_AAAA, + dnsTtlSec: 30, + loadBalancer: true +}); + +const loadbalancer = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc, internetFacing: true }); + +service.registerLoadBalancer("Loadbalancer", loadbalancer); + +app.run(); diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-public-dns-namespace.lit.expected.json b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-public-dns-namespace.lit.expected.json new file mode 100644 index 0000000000000..36f9e2ada5061 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-public-dns-namespace.lit.expected.json @@ -0,0 +1,58 @@ +{ + "Resources": { + "Namespace9B63B8C8": { + "Type": "AWS::ServiceDiscovery::PublicDnsNamespace", + "Properties": { + "Name": "foobar.com" + } + }, + "NamespaceServiceCABDF534": { + "Type": "AWS::ServiceDiscovery::Service", + "Properties": { + "DnsConfig": { + "DnsRecords": [ + { + "TTL": "30", + "Type": "A" + } + ], + "NamespaceId": { + "Fn::GetAtt": [ + "Namespace9B63B8C8", + "Id" + ] + }, + "RoutingPolicy": "MULTIVALUE" + }, + "HealthCheckConfig": { + "FailureThreshold": 2, + "ResourcePath": "/healthcheck", + "Type": "HTTPS" + }, + "Name": "foo", + "NamespaceId": { + "Fn::GetAtt": [ + "Namespace9B63B8C8", + "Id" + ] + } + } + }, + "NamespaceServiceIpInstanceCCED93E7": { + "Type": "AWS::ServiceDiscovery::Instance", + "Properties": { + "InstanceAttributes": { + "AWS_INSTANCE_IPV4": "54.239.25.192", + "AWS_INSTANCE_PORT": "443" + }, + "ServiceId": { + "Fn::GetAtt": [ + "NamespaceServiceCABDF534", + "Id" + ] + }, + "InstanceId": "awsservicediscoveryintegNamespaceServiceIpInstance5A6845D4" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-public-dns-namespace.lit.ts b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-public-dns-namespace.lit.ts new file mode 100644 index 0000000000000..04ed6e9b19a78 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-public-dns-namespace.lit.ts @@ -0,0 +1,27 @@ +import cdk = require('@aws-cdk/cdk'); +import servicediscovery = require('../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-servicediscovery-integ'); + +const namespace = new servicediscovery.PublicDnsNamespace(stack, 'Namespace', { + name: 'foobar.com', +}); + +const service = namespace.createService('Service', { + name: 'foo', + dnsRecordType: servicediscovery.DnsRecordType.A, + dnsTtlSec: 30, + healthCheck: { + type: servicediscovery.HealthCheckType.Https, + resourcePath: '/healthcheck', + failureThreshold: 2 + } +}); + +service.registerIpInstance({ + ipv4: '54.239.25.192', + port: 443 +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-servicediscovery/test/test.instance.ts b/packages/@aws-cdk/aws-servicediscovery/test/test.instance.ts new file mode 100644 index 0000000000000..bc17e6fae4ba8 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/test.instance.ts @@ -0,0 +1,477 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import elbv2 = require('@aws-cdk/aws-elasticloadbalancingv2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import servicediscovery = require('../lib'); + +export = { + 'IpInstance for service in HTTP namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.HttpNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + const service = namespace.createService('MyService', { + name: 'service', + }); + + service.registerIpInstance({ + ipv4: '10.0.0.0', + ipv6: '0:0:0:0:0:ffff:a00:0', + port: 443 + }); + + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::Instance', { + InstanceAttributes: { + AWS_INSTANCE_IPV4: '10.0.0.0', + AWS_INSTANCE_IPV6: '0:0:0:0:0:ffff:a00:0', + AWS_INSTANCE_PORT: '443' + }, + ServiceId: { + 'Fn::GetAtt': [ + 'MyNamespaceMyService365E2470', + 'Id' + ] + }, + InstanceId: 'MyNamespaceMyServiceIpInstanceBACEB9D2' + })); + + test.done(); + }, + + 'IpInstance for service in PublicDnsNamespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'public', + }); + + const service = namespace.createService('MyService', { + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.A_AAAA + }); + + service.registerIpInstance({ + ipv4: '54.239.25.192', + ipv6: '0:0:0:0:0:ffff:a00:0', + port: 443 + }); + + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::Instance', { + InstanceAttributes: { + AWS_INSTANCE_IPV4: '54.239.25.192', + AWS_INSTANCE_IPV6: '0:0:0:0:0:ffff:a00:0', + AWS_INSTANCE_PORT: '443' + }, + ServiceId: { + 'Fn::GetAtt': [ + 'MyNamespaceMyService365E2470', + 'Id' + ] + }, + InstanceId: 'MyNamespaceMyServiceIpInstanceBACEB9D2' + })); + + test.done(); + }, + + 'IpInstance for service in PrivateDnsNamespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); + + const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'MyNamespace', { + name: 'public', + vpc + }); + + const service = namespace.createService('MyService', { + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.A_AAAA + }); + + service.registerIpInstance({ + ipv4: '10.0.0.0', + ipv6: '0:0:0:0:0:ffff:a00:0', + port: 443 + }); + + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::Instance', { + InstanceAttributes: { + AWS_INSTANCE_IPV4: '10.0.0.0', + AWS_INSTANCE_IPV6: '0:0:0:0:0:ffff:a00:0', + AWS_INSTANCE_PORT: '443' + }, + ServiceId: { + 'Fn::GetAtt': [ + 'MyNamespaceMyService365E2470', + 'Id' + ] + }, + InstanceId: 'MyNamespaceMyServiceIpInstanceBACEB9D2' + })); + + test.done(); + }, + + 'Registering IpInstance throws when omitting port for a service using SRV'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'public', + }); + + const service = namespace.createService('MyService', { + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.SRV + }); + + // THEN + test.throws(() => { + service.registerIpInstance({ + instanceId: 'id', + }); + }, /A `port` must be specified for a service using a `SRV` record./); + + test.done(); + }, + + 'Registering IpInstance throws when omitting ipv4 and ipv6 for a service using SRV'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'dns', + }); + + const service = namespace.createService('MyService', { + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.SRV + }); + + // THEN + test.throws(() => { + service.registerIpInstance({ + port: 3306 + }); + }, /At least `ipv4` or `ipv6` must be specified for a service using a `SRV` record./); + + test.done(); + }, + + 'Registering IpInstance throws when omitting ipv4 for a service using A records'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'dns', + }); + + const service = namespace.createService('MyService', { + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.A + }); + + // THEN + test.throws(() => { + service.registerIpInstance({ + port: 3306 + }); + }, /An `ipv4` must be specified for a service using a `A` record./); + + test.done(); + }, + + 'Registering IpInstance throws when omitting ipv6 for a service using AAAA records'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'dns', + }); + + const service = namespace.createService('MyService', { + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.AAAA + }); + + // THEN + test.throws(() => { + service.registerIpInstance({ + port: 3306 + }); + }, /An `ipv6` must be specified for a service using a `AAAA` record./); + + test.done(); + }, + + 'Registering IpInstance throws with wrong DNS record type'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'dns', + }); + + const service = namespace.createService('MyService', { + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.CNAME + }); + + // THEN + test.throws(() => { + service.registerIpInstance({ + port: 3306 + }); + }, /Service must support `A`, `AAAA` or `SRV` records to register this instance type./); + + test.done(); + }, + + 'Registering AliasTargetInstance'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const vpc = new ec2.VpcNetwork(stack, 'MyVPC'); + const alb = new elbv2.ApplicationLoadBalancer(stack, 'MyALB', { vpc }); + + const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'MyNamespace', { + name: 'dns', + vpc + }); + + const service = namespace.createService('MyService', { + name: 'service', + loadBalancer: true, + }); + const customAttributes = { foo: 'bar' }; + + service.registerLoadBalancer("Loadbalancer", alb, customAttributes); + + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::Instance', { + InstanceAttributes: { + AWS_ALIAS_DNS_NAME: { + 'Fn::GetAtt': [ + 'MyALB911A8556', + 'DNSName' + ] + }, + foo: 'bar' + }, + ServiceId: { + 'Fn::GetAtt': [ + 'MyNamespaceMyService365E2470', + 'Id' + ] + }, + InstanceId: 'MyNamespaceMyServiceLoadbalancerD1112A76' + })); + + test.done(); + }, + + 'Throws when registering AliasTargetInstance with Http Namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.HttpNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + }); + + const vpc = new ec2.VpcNetwork(stack, 'MyVPC'); + const alb = new elbv2.ApplicationLoadBalancer(stack, 'MyALB', { vpc }); + + // THEN + test.throws(() => { + service.registerLoadBalancer("Loadbalancer", alb); + }, /Namespace associated with Service must be a DNS Namespace./); + + test.done(); + }, + + // TODO shouldn't be allowed to do this if loadbalancer on ServiceProps is not set to true. + 'Throws when registering AliasTargetInstance with wrong Routing Policy'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + const service = namespace.createService('MyService', { + routingPolicy: servicediscovery.RoutingPolicy.Multivalue + }); + + const vpc = new ec2.VpcNetwork(stack, 'MyVPC'); + const alb = new elbv2.ApplicationLoadBalancer(stack, 'MyALB', { vpc }); + + // THEN + test.throws(() => { + service.registerLoadBalancer("Loadbalancer", alb); + }, /Service must use `WEIGHTED` routing policy./); + + test.done(); + }, + + 'Register CnameInstance'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'dns', + }); + + const service = namespace.createService('MyService', { + dnsRecordType: servicediscovery.DnsRecordType.CNAME + }); + + service.registerCnameInstance({ + instanceCname: 'foo.com', + customAttributes: { dogs: 'good' } + }); + + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::Instance', { + InstanceAttributes: { + AWS_INSTANCE_CNAME: 'foo.com', + dogs: 'good' + }, + ServiceId: { + 'Fn::GetAtt': [ + 'MyNamespaceMyService365E2470', + 'Id' + ] + }, + InstanceId: 'MyNamespaceMyServiceCnameInstance0EB1C98D' + })); + + test.done(); + }, + + 'Throws when registering CnameInstance for an HTTP namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.HttpNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + }); + + // THEN + test.throws(() => { + service.registerCnameInstance({ + instanceCname: 'foo.com', + }); + }, /Namespace associated with Service must be a DNS Namespace/); + + test.done(); + }, + + 'Register NonIpInstance'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.HttpNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + const service = namespace.createService('MyService'); + + service.registerNonIpInstance({ + customAttributes: { dogs: 'good' } + }); + + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::Instance', { + InstanceAttributes: { + dogs: 'good' + }, + ServiceId: { + 'Fn::GetAtt': [ + 'MyNamespaceMyService365E2470', + 'Id' + ] + }, + InstanceId: 'MyNamespaceMyServiceNonIpInstance7EFD703A' + })); + + test.done(); + }, + + 'Throws when registering NonIpInstance for an Public namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + const service = namespace.createService('MyService'); + + // THEN + test.throws(() => { + service.registerNonIpInstance({ + instanceId: 'nonIp', + }); + }, /This type of instance can only be registered for HTTP namespaces./); + + test.done(); + }, + + 'Throws when no custom attribues specified for NonIpInstance'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.HttpNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + const service = namespace.createService('MyService'); + + // THEN + test.throws(() => { + service.registerNonIpInstance({ + instanceId: 'nonIp', + }); + }, /You must specify at least one custom attribute for this instance type./); + + test.done(); + }, + + 'Throws when custom attribues are emptyfor NonIpInstance'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.HttpNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + const service = namespace.createService('MyService'); + + // THEN + test.throws(() => { + service.registerNonIpInstance({ + instanceId: 'nonIp', + customAttributes: {} + }); + }, /You must specify at least one custom attribute for this instance type./); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/aws-servicediscovery/test/test.namespace.ts b/packages/@aws-cdk/aws-servicediscovery/test/test.namespace.ts new file mode 100644 index 0000000000000..4807f46a22061 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/test.namespace.ts @@ -0,0 +1,68 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import servicediscovery = require('../lib'); + +export = { + 'HTTP namespace'(test: Test) { + const stack = new cdk.Stack(); + + new servicediscovery.HttpNamespace(stack, 'MyNamespace', { + name: 'foobar.com', + }); + + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::HttpNamespace", + Properties: { + Name: "foobar.com" + } + } + } + }); + + test.done(); + }, + + 'Public DNS namespace'(test: Test) { + const stack = new cdk.Stack(); + + new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'foobar.com', + }); + + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::PublicDnsNamespace", + Properties: { + Name: "foobar.com" + } + } + } + }); + + test.done(); + }, + + 'Private DNS namespace'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); + + new servicediscovery.PrivateDnsNamespace(stack, 'MyNamespace', { + name: 'foobar.com', + vpc + }); + + expect(stack).to(haveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + Name: "foobar.com", + Vpc: { + Ref: "MyVpcF9F0CA6F" + } + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-servicediscovery/test/test.service.ts b/packages/@aws-cdk/aws-servicediscovery/test/test.service.ts new file mode 100644 index 0000000000000..f09c336ce53fb --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/test.service.ts @@ -0,0 +1,453 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import servicediscovery = require('../lib'); + +export = { + 'Service for HTTP namespace with custom health check'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.HttpNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + namespace.createService('MyService', { + name: 'service', + description: 'service description', + customHealthCheck: { + failureThreshold: 3, + } + }); + + // THEN + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::HttpNamespace", + Properties: { + Name: "http" + }, + }, + MyNamespaceMyService365E2470: { + Type: "AWS::ServiceDiscovery::Service", + Properties: { + Description: "service description", + HealthCheckCustomConfig: { + FailureThreshold: 3, + }, + Name: "service", + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + } + } + } + } + }); + + test.done(); + }, + + 'Service for HTTP namespace with health check'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.HttpNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + namespace.createService('MyService', { + name: 'service', + description: 'service description', + healthCheck: { + type: servicediscovery.HealthCheckType.Http, + resourcePath: '/check' + } + }); + + // THEN + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::HttpNamespace", + Properties: { + Name: "http" + }, + }, + MyNamespaceMyService365E2470: { + Type: "AWS::ServiceDiscovery::Service", + Properties: { + Description: "service description", + HealthCheckConfig: { + FailureThreshold: 1, + ResourcePath: "/check", + Type: "HTTP" + }, + Name: "service", + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + } + } + } + } + }); + + test.done(); + }, + + 'Service for Public DNS namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'dns', + }); + + namespace.createService('MyService', { + name: 'service', + description: 'service description', + customHealthCheck: { + failureThreshold: 3, + } + }); + + // THEN + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::PublicDnsNamespace", + Properties: { + Name: "dns" + } + }, + MyNamespaceMyService365E2470: { + Type: "AWS::ServiceDiscovery::Service", + Properties: { + Description: "service description", + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "A" + } + ], + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + }, + RoutingPolicy: "MULTIVALUE" + }, + HealthCheckCustomConfig: { + FailureThreshold: 3 + }, + Name: "service", + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + } + } + } + } + }); + + test.done(); + }, + + 'Service for Public DNS namespace with A and AAAA records'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'dns', + }); + + namespace.createService('MyService', { + dnsRecordType: servicediscovery.DnsRecordType.A_AAAA + }); + + // THEN + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::PublicDnsNamespace", + Properties: { + Name: "dns" + } + }, + MyNamespaceMyService365E2470: { + Type: "AWS::ServiceDiscovery::Service", + Properties: { + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "A" + }, + { + TTL: "60", + Type: "AAAA" + } + ], + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + }, + RoutingPolicy: "MULTIVALUE", + }, + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + } + } + } + } + }); + + test.done(); + }, + + 'Defaults to WEIGHTED routing policy for CNAME'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'dns', + }); + + namespace.createService('MyService', { + dnsRecordType: servicediscovery.DnsRecordType.CNAME + }); + + // THEN + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::PublicDnsNamespace", + Properties: { + Name: "dns" + } + }, + MyNamespaceMyService365E2470: { + Type: "AWS::ServiceDiscovery::Service", + Properties: { + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "CNAME" + } + ], + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + }, + RoutingPolicy: "WEIGHTED", + }, + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + } + } + } + } + }); + + test.done(); + }, + + 'Throws when specifying both healthCheckConfig and healthCheckCustomConfig on PublicDnsNamespace'(test: Test) { + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'name', + }); + + // THEN + test.throws(() => { + namespace.createService('MyService', { + name: 'service', + healthCheck: { + resourcePath: '/' + }, + customHealthCheck: { + failureThreshold: 1 + } + }); + }, /`healthCheckConfig`.+`healthCheckCustomConfig`/); + + test.done(); + }, + + 'Throws when specifying healthCheckConfig on PrivateDnsNamespace'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); + + const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'MyNamespace', { + name: 'name', + vpc + }); + + // THEN + test.throws(() => { + namespace.createService('MyService', { + name: 'service', + healthCheck: { + resourcePath: '/' + }, + customHealthCheck: { + failureThreshold: 1 + } + }); + }, /`healthCheckConfig`.+`healthCheckCustomConfig`/); + + test.done(); + }, + + 'Throws when using CNAME and Multivalue routing policy'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'name', + }); + + // THEN + test.throws(() => { + namespace.createService('MyService', { + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.CNAME, + routingPolicy: servicediscovery.RoutingPolicy.Multivalue, + }); + }, /Cannot use `CNAME` record when routing policy is `Multivalue`./); + + test.done(); + }, + + 'Throws when specifying resourcePath with TCP'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'name', + }); + + // THEN + test.throws(() => { + namespace.createService('MyService', { + name: 'service', + healthCheck: { + type: servicediscovery.HealthCheckType.Tcp, + resourcePath: '/check' + } + }); + }, /`resourcePath`.+`TCP`/); + + test.done(); + }, + + 'Throws when specifying loadbalancer with wrong DnsRecordType'(test: Test) { + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'name', + }); + + // THEN + test.throws(() => { + namespace.createService('MyService', { + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.CNAME, + loadBalancer: true + }); + }, /Must support `A` or `AAAA` records to register loadbalancers/); + + test.done(); + }, + + 'Throws when specifying loadbalancer with Multivalue routing Policy'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.PublicDnsNamespace(stack, 'MyNamespace', { + name: 'http', + }); + + // THEN + test.throws(() => { + namespace.createService('MyService', { + loadBalancer: true, + routingPolicy: servicediscovery.RoutingPolicy.Multivalue + }); + }, /Cannot register loadbalancers when routing policy is `Multivalue`./); + + test.done(); + }, + + 'Service for Private DNS namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); + + const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'MyNamespace', { + name: 'private', + vpc + }); + + namespace.createService('MyService', { + name: 'service', + description: 'service description', + }); + + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + Name: "private" + })); + + expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + Description: "service description", + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "A" + } + ], + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + }, + RoutingPolicy: "MULTIVALUE" + }, + Name: "service", + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + } + })); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/aws-servicediscovery/test/test.servicediscovery.ts b/packages/@aws-cdk/aws-servicediscovery/test/test.servicediscovery.ts deleted file mode 100644 index 820f6b467f38f..0000000000000 --- a/packages/@aws-cdk/aws-servicediscovery/test/test.servicediscovery.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -export = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -});