diff --git a/packages/@aws-cdk/aws-servicediscovery/README.md b/packages/@aws-cdk/aws-servicediscovery/README.md index 82818175c1dc5..072cd72853d6a 100644 --- a/packages/@aws-cdk/aws-servicediscovery/README.md +++ b/packages/@aws-cdk/aws-servicediscovery/README.md @@ -1,2 +1,11 @@ ## 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) 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..b0931677bee74 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/http-namespace.ts @@ -0,0 +1,41 @@ +import cdk = require('@aws-cdk/cdk'); +import { CfnHttpNamespace } from './servicediscovery.generated'; + +export interface HttpNamespaceProps { + /** + * A name for the HttpNamespace. + */ + name: string; + + /** + * A description of the namespace. + */ + description?: string; +} + +/** + * Define a Service Discovery HTTP Namespace + */ +export class HttpNamespace extends cdk.Construct { + /** + * A domain name + */ + public readonly name: string; + + /** + * Namespace Id for the HttpNamespace. + */ + public readonly namespaceId: string; + + constructor(scope: cdk.Construct, id: string, props: HttpNamespaceProps) { + super(scope, id); + + const ns = new CfnHttpNamespace(this, 'Resource', { + name: props.name, + description: props.description + }); + + this.name = props.name; + this.namespaceId = ns.httpNamespaceId; + } +} diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/index.ts b/packages/@aws-cdk/aws-servicediscovery/lib/index.ts index 3683de632dddb..ec437108f3b9b 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/index.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/index.ts @@ -1,2 +1,5 @@ +export * from './instance'; +export * from './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..3291db2660a8d --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/instance.ts @@ -0,0 +1,168 @@ +import cdk = require('@aws-cdk/cdk'); +import { NamespaceType } from './namespace'; +import { DnsRecordType, Service } from './service'; +import { CfnInstance } from './servicediscovery.generated'; + +/** + * Properties to define ServiceDiscovery Instance + */ +export interface InstanceProps { + /** + * The instance attributes. + * FIXME: add defaults/validations + */ + instanceAttributes: InstanceAttributes + + /** + * The id of the instance resource + */ + instanceId: string; + + /** + * The Cloudmap service this resource is registered to. + */ + service: Service; +} + +/** + * Define a Service Discovery Instance + */ +export class Instance extends cdk.Construct { + /** + * The Id of the instance + */ + public readonly instanceId: string; + /** + * The Cloudmap service to which the instance is registered. + */ + public readonly service: Service; + + constructor(scope: cdk.Construct, id: string, props: InstanceProps) { + super(scope, id); + + const customAttributes = props.instanceAttributes.customAttributes || {}; + + const resource = new CfnInstance(this, 'Resource', { + instanceAttributes: { ...customAttributes, ...getInstanceAttributes(props) }, + instanceId: props.instanceId, + serviceId: props.service.serviceId + }); + + this.service = props.service; + this.instanceId = resource.instanceId; + } +} + +// NOTE: These are the 5 that seem to be supported in cloudformation, but the API docs indicate that you can also +// specify custom attributes. Not sure if CFN would support these? In the generated L1s instance attributes appears to +// just be an object. +export interface InstanceAttributes { + /** + * If you want AWS Cloud Map to create an Amazon Route 53 alias record that routes traffic to an Elastic Load + * Balancing load balancer, specify the DNS name that is associated with the load balancer. + */ + aliasDnsName?: string, + + /** + * 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, + + /** 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. + */ + port?: string, + /** + * 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. + */ + 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. + */ + ipv6?: string, + + /** + * Custom attributes of the instance. + */ + customAttributes?: object; +} + +/** + * Validates instance attributes and returns standard attributes based on the namespace/service type. + * + * @param props instance props + * @throws if the instance attributes are invalid + */ +function getInstanceAttributes(props: InstanceProps): object { + if (props.instanceAttributes.aliasDnsName && props.instanceAttributes.instanceCname) { + throw new Error('Cannot specify both `aliasDnsName` and `instanceCname`.'); + } + + if (props.service.namespace.type === NamespaceType.Http) { + if (props.instanceAttributes.aliasDnsName || props.instanceAttributes.instanceCname) { + throw new Error('Cannot specify `aliasDnsName` or `instanceCname` for an HTTP namespace.'); + } + + return { + AWS_INSTANCE_IPV4: props.instanceAttributes.ipv4, + AWS_INSTANCE_IPV6: props.instanceAttributes.ipv6, + AWS_INSTANCE_PORT: props.instanceAttributes.port + }; + } + + if (props.service.dnsRecordType === DnsRecordType.Cname) { + if (!props.instanceAttributes.instanceCname) { + throw new Error('A `instanceCname` must be specified for a service using a `CNAME` record.'); + } + + return { + AWS_INSTANCE_CNAME: props.instanceAttributes.instanceCname + }; + } + + if (props.service.dnsRecordType === DnsRecordType.Srv) { + if (!props.instanceAttributes.port) { + throw new Error('A `port` must be specified for a service using a `SRV` record.'); + } + + if (!props.instanceAttributes.ipv4 && !props.instanceAttributes.ipv6) { + throw new Error('At least `ipv4` or `ipv6` must be specified for a service using a `SRV` record.'); + } + + return { + AWS_INSTANCE_IPV4: props.instanceAttributes.ipv4, + AWS_INSTANCE_IPV6: props.instanceAttributes.ipv6, + AWS_INSTANCE_PORT: props.instanceAttributes.port + }; + } + + if (props.instanceAttributes.aliasDnsName) { + if (props.instanceAttributes.ipv4 || props.instanceAttributes.ipv6 || props.instanceAttributes.port) { + throw new Error('Cannot specify `ipv4`, `ipv6` or `port` when specifying `aliasDnsName`.'); + } + + return { + AWS_ALIAS_DNS_NAME: props.instanceAttributes.aliasDnsName + }; + } + + if (!props.instanceAttributes.ipv4 && (props.service.dnsRecordType === DnsRecordType.A || props.service.dnsRecordType === DnsRecordType.A_AAAA)) { + throw new Error('An `ipv4` must be specified for a service using a `A` record.'); + } + + if (!props.instanceAttributes.ipv6 && + (props.service.dnsRecordType === DnsRecordType.AAAA || props.service.dnsRecordType === DnsRecordType.A_AAAA)) { + throw new Error('An `ipv6` must be specified for a service using a `AAAA` record.'); + } + + return { + AWS_INSTANCE_IPV4: props.instanceAttributes.ipv4, + AWS_INSTANCE_IPV6: props.instanceAttributes.ipv6, + AWS_INSTANCE_PORT: props.instanceAttributes.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..1489e50813ef3 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/namespace.ts @@ -0,0 +1,156 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +// import { BaseServiceProps, Service } from './service'; +import { CfnHttpNamespace, CfnPrivateDnsNamespace, CfnPublicDnsNamespace} from './servicediscovery.generated'; + +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. Valid values: HTTP, DNS_PUBLIC, or DNS_PRIVATE + */ + readonly type: NamespaceType; +} + +export interface NamespaceProps { + /** + * A name for the Namespace. + */ + name: string; + + /** + * A description of the Namespace. + * + * @default none + */ + description?: string; + + /** + * Type of Namespace. Valid values: HTTP, DNS_PUBLIC, or DNS_PRIVATE + * + * @default HTTP + */ + type?: NamespaceType; + + /** + * The Amazon VPC that you want to associate the namespace with. + * Only applies for Private DNS Namespaces. + * + * @default none + */ + vpc?: ec2.IVpcNetwork; + +} + +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 { + Http = "HTTP", + DnsPublic = "DNS_PUBLIC", + DnsPrivate = "DNS_PRIVATE" +} + +export class Namespace extends cdk.Construct implements INamespace { + public readonly namespaceName: string; + public readonly namespaceId: string; + public readonly namespaceArn: string; + public readonly type: NamespaceType; + + constructor(scope: cdk.Construct, id: string, props: NamespaceProps) { + super(scope, id); + + const namespaceName = props.name; + const description = props.description; + + let namespaceType = props.type; + if (namespaceType === undefined) { + namespaceType = NamespaceType.Http; + } + + this.namespaceName = namespaceName; + this.type = namespaceType; + + switch (namespaceType) { + case NamespaceType.Http: { + const ns = new CfnHttpNamespace(this, 'Resource', { + name: namespaceName, + description + }); + + this.namespaceId = ns.httpNamespaceId; + this.namespaceArn = ns.httpNamespaceArn; + break; + } + + case NamespaceType.DnsPrivate: { + if (props.vpc === undefined) { + throw new Error(`VPC must be specified for PrivateDNSNamespaces`); + } + + const ns = new CfnPrivateDnsNamespace(this, 'Resource', { + name: namespaceName, + description, + vpc: props.vpc.vpcId + }); + + this.namespaceId = ns.privateDnsNamespaceId; + this.namespaceArn = ns.privateDnsNamespaceArn; + break; + } + + case NamespaceType.DnsPublic: { + const ns = new CfnPublicDnsNamespace(this, 'Resource', { + name: namespaceName, + description + }); + + this.namespaceId = ns.publicDnsNamespaceId; + this.namespaceArn = ns.publicDnsNamespaceArn; + break; + } + } + } + + /** + * Creates a new service in this namespace FIXME -- not setting namespace correctly + */ + // public createService(id: string, props?: BaseServiceProps): Service { + // return new Service(this, id, { + // namespace: this, + // ...props, + // }); + // } +} 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..b519c12918233 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/private-dns-namespace.ts @@ -0,0 +1,54 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { CfnPrivateDnsNamespace} from './servicediscovery.generated'; + +export interface PrivateDnsNamespaceProps { + /** + * A name for the HttpNamespace. + */ + name: string; + + /** + * The Amazon VPC that you want to associate the namespace with. + */ + vpc: ec2.IVpcNetwork; + + /** + * A description of the namespace. + */ + description?: string; +} + +/** + * Define a Service Discovery HTTP Namespace + */ +export class PrivateDnsNamespace extends cdk.Construct { + /** + * A name for the PrivateDnsNamespace. + */ + public readonly name: string; + + /** + * Namespace Id for the PrivateDnsNamespace. + */ + public readonly namespaceId: string; + + /** + * The Amazon VPC that you want to associate the namespace with. + */ + public readonly vpc: ec2.IVpcNetwork; + + constructor(scope: cdk.Construct, id: string, props: PrivateDnsNamespaceProps) { + super(scope, id); + + const ns = new CfnPrivateDnsNamespace(this, 'Resource', { + name: props.name, + description: props.description, + vpc: props.vpc.vpcId + }); + + this.vpc = props.vpc; + this.name = props.name; + this.namespaceId = ns.privateDnsNamespaceId; + } +} 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..530a52a02a077 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/public-dns-namespace.ts @@ -0,0 +1,41 @@ +import cdk = require('@aws-cdk/cdk'); +import { CfnPublicDnsNamespace} from './servicediscovery.generated'; + +export interface PublicDnsNamespaceProps { + /** + * A name for the HttpNamespace. + */ + name: string; + + /** + * A description of the namespace. + */ + description?: string; +} + +/** + * Define a Service Discovery HTTP Namespace + */ +export class PublicDnsNamespace extends cdk.Construct { + /** + * A name for the PublicDnsNamespace. + */ + public readonly name: string; + + /** + * Namespace Id for the PublicDnsNamespace. + */ + public readonly namespaceId: string; + + constructor(scope: cdk.Construct, id: string, props: PublicDnsNamespaceProps) { + super(scope, id); + + const ns = new CfnPublicDnsNamespace(this, 'Resource', { + name: props.name, + description: props.description, + }); + + this.name = props.name; + this.namespaceId = ns.publicDnsNamespaceId; + } +} 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..c05f2043bb51d --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/lib/service.ts @@ -0,0 +1,300 @@ +import cdk = require('@aws-cdk/cdk'); +import { INamespace, NamespaceType } from './namespace'; +import { CfnService} from './servicediscovery.generated'; + +export interface 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; + + /** + * A complex type that contains settings for an optional health check. + * If you specify settings for a health check, AWS Cloud Map associates the health check with the + * records that you specify in DnsConfig. + * Public DNS namespaces only. Only one of healthCheckConfig or healthCheckCustomConfig can be specified. + * + * @default none + */ + healthCheckConfig?: 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 + */ + healthCheckCustomConfig?: HealthCheckCustomConfig; + + /** + * 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, MULTIVALUE otherwise + */ + routingPolicy?: RoutingPolicy; + + /** + * A name for the Service. + * + * @default CloudFormation-generated name + */ + name?: string; + + /** + * A description of the service. + * + * @default none + */ + description?: string; +} + +export interface ServiceProps extends BaseServiceProps { + /** + * 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 { + /** + * A name for the Cloudmap Service. + */ + public readonly name: 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; + + 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.healthCheckConfig && props.healthCheckCustomConfig) { + throw new Error('Cannot specify both `healthCheckConfig` and `healthCheckCustomConfig`.'); + } + + if (namespaceType !== NamespaceType.DnsPublic && props.healthCheckConfig) { + throw new Error('Can only use `healthCheckConfig` for a Public DNS namespace.'); + } + if (props.routingPolicy === RoutingPolicy.Multivalue + && props.dnsRecordType === DnsRecordType.Cname) { + throw new Error('Cannot use `CNAME` record when routing policy is `Multivalue`.'); + } + + if (props.healthCheckConfig + && props.healthCheckConfig.type === HealthCheckType.Tcp + && props.healthCheckConfig.resourcePath) { + throw new Error('Cannot specify `resourcePath` when using a `TCP` health check.'); + } + + // Set defaults where necessary + const routingPolicy = props.dnsRecordType === DnsRecordType.Cname + ? RoutingPolicy.Weighted + : RoutingPolicy.Multivalue; + + const dnsRecordType = props.dnsRecordType !== undefined ? props.dnsRecordType : DnsRecordType.A; + + const dnsConfig = props.namespace.type === NamespaceType.Http + ? undefined + : { + dnsRecords: [ + { + type: dnsRecordType, + ttl: props.dnsTtlSec !== undefined ? props.dnsTtlSec.toString() : '60', + } + ], + namespaceId: props.namespace.namespaceId, + routingPolicy, + }; + + const healthCheckConfigDefaults = { + type: HealthCheckType.Http, + failureThreshold: 1, + resourcePath: props.healthCheckConfig && props.healthCheckConfig.type !== HealthCheckType.Tcp + ? '/' + : undefined + }; + + const healthCheckConfig = props.healthCheckConfig + ? { ...healthCheckConfigDefaults, ...props.healthCheckConfig } + : undefined; + + const healthCheckCustomConfig = props.healthCheckCustomConfig + ? props.healthCheckCustomConfig + : undefined; + + // Create service + const service = new CfnService(this, 'Resource', { + name: props.name, + description: props.description, + dnsConfig, + healthCheckConfig, + healthCheckCustomConfig, + namespaceId: props.namespace.namespaceId + }); + + this.name = service.serviceName; + this.serviceArn = service.serviceArn; + this.serviceId = service.serviceId; + this.namespace = props.namespace; + this.dnsRecordType = props.dnsRecordType || DnsRecordType.A; + } +} + +export interface DnsConfig { + dnsRecords: DnsRecord[], + namespaceId: string, + /** + * The routing policy that you want to apply to all Route 53 DNS records that AWS Cloud Map creates when you + * register an instance and specify this service. + */ + routingPolicy?: RoutingPolicy +} + +export interface DnsRecord { + /** + * The record type + */ + type: DnsRecordType, + + /** + * The time to live for the record + */ + ttlSec: number, +} + +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. + * + * @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, +} + +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 50e64f01ecc1a..fee2de6185bc6 100644 --- a/packages/@aws-cdk/aws-servicediscovery/package.json +++ b/packages/@aws-cdk/aws-servicediscovery/package.json @@ -60,13 +60,26 @@ "pkglint": "^0.25.1" }, "dependencies": { - "@aws-cdk/cdk": "^0.25.1" + "@aws-cdk/cdk": "^0.25.1", + "@aws-cdk/aws-ec2": "^0.25.1", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.25.1" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { - "@aws-cdk/cdk": "^0.25.1" + "@aws-cdk/cdk": "^0.25.1", + "@aws-cdk/aws-ec2": "^0.25.1", + "@aws-cdk/aws-elasticloadbalancingv2": "^0.25.1" }, "engines": { "node": ">= 8.10.0" + }, + "awslint": { + "exclude": [ + "resource-class:@aws-cdk/aws-servicediscovery.HttpNamespace", + "resource-interface:@aws-cdk/aws-servicediscovery.IInstance", + "resource-class:@aws-cdk/aws-servicediscovery.PrivateDnsNamespace", + "resource-class:@aws-cdk/aws-servicediscovery.PublicDnsNamespace", + "resource-interface:@aws-cdk/aws-servicediscovery.IService" + ] } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.ts b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.ts new file mode 100644 index 0000000000000..92389411667dd --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-http-namespace.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.Namespace(stack, 'MyNamespace', { + name: 'covfefe', + type: servicediscovery.NamespaceType.Http, +}); + +new servicediscovery.Service(stack, 'Service', { + name: 'service', + namespace, + description: 'service description', + healthCheckCustomConfig: { + failureThreshold: 3, + } +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.ts b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.ts new file mode 100644 index 0000000000000..d76fa6aecb7d8 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.ts @@ -0,0 +1,21 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +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.Namespace(stack, 'Namespace', { + name: "foobar.com", + vpc, + type: servicediscovery.NamespaceType.DnsPrivate +}); + +new servicediscovery.Service(stack, 'Service', { + name: "frontend", + namespace, +}); + +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..cb88b2078dd93 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/test.instance.ts @@ -0,0 +1,355 @@ +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 = { + 'Instance for service in HTTP namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'http', + type: servicediscovery.NamespaceType.Http, + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + }); + + new servicediscovery.Instance(stack, 'MyInstance', { + service, + instanceId: "41332780-d796-feed-face-02252334a661", + instanceAttributes: { + 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": [ + "MyServiceA1F951EB", + "Id" + ] + }, + InstanceId: "41332780-d796-feed-face-02252334a661" + })); + + test.done(); + }, + + 'Instance for a load balancer'(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.Namespace(stack, 'MyNamespace', { + name: 'dns', + type: servicediscovery.NamespaceType.DnsPrivate, + vpc + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + }); + + new servicediscovery.Instance(stack, 'MyInstance', { + service, + instanceId: 'id', + instanceAttributes: { + aliasDnsName: alb.asAliasRecordTarget().dnsName + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::Instance', { + InstanceAttributes: { + AWS_ALIAS_DNS_NAME: { + "Fn::GetAtt": [ + "MyALB911A8556", + "DNSName" + ] + } + }, + ServiceId: { + "Fn::GetAtt": [ + "MyServiceA1F951EB", + "Id" + ] + }, + InstanceId: "id" + })); + + test.done(); + }, + + 'Instance with domain name'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'dns', + type: servicediscovery.NamespaceType.DnsPublic + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + dnsRecordType: servicediscovery.DnsRecordType.Cname + }); + + new servicediscovery.Instance(stack, 'MyInstance', { + instanceId: 'id', + service, + instanceAttributes: { + instanceCname: 'foo.com' + } + }); + + // THEN + expect(stack).to(haveResource('AWS::ServiceDiscovery::Instance', { + InstanceAttributes: { + AWS_INSTANCE_CNAME: "foo.com", + }, + ServiceId: { + "Fn::GetAtt": [ + "MyServiceA1F951EB", + "Id" + ] + }, + InstanceId: "id" + })); + + test.done(); + }, + + 'Throws when specifying both aliasDnsName and instanceCname'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'http', + type: servicediscovery.NamespaceType.Http + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + }); + + // THEN + test.throws(() => { + new servicediscovery.Instance(stack, 'MyInstance', { + service, + instanceId: 'id', + instanceAttributes: { + aliasDnsName: 'alb.foo.com', + instanceCname: 'domain' + } + }); + }, /Cannot specify both `aliasDnsName` and `instanceCname`/); + + test.done(); + }, + + 'Throws when specifying aliasDnsName for an HTTP only namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'http', + type: servicediscovery.NamespaceType.Http + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + }); + + // THEN + test.throws(() => { + new servicediscovery.Instance(stack, 'MyInstance', { + service, + instanceId: 'id', + instanceAttributes: { + aliasDnsName: "foo.com", + } + }); + }, /Cannot specify `aliasDnsName` or `instanceCname` for an HTTP namespace./); + + test.done(); + }, + + 'Throws when specifying instanceCname for an HTTP namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'http', + type: servicediscovery.NamespaceType.Http + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + }); + + // THEN + test.throws(() => { + new servicediscovery.Instance(stack, 'MyInstance', { + service, + instanceId: 'id', + instanceAttributes: { + instanceCname: "domain", + } + }); + }, /Cannot specify `aliasDnsName` or `instanceCname` for an HTTP namespace./); + + test.done(); + }, + + 'Throws when omitting instanceCname for a service using CNAME'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'dns', + type: servicediscovery.NamespaceType.DnsPublic, + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + dnsRecordType: servicediscovery.DnsRecordType.Cname, + dnsTtlSec: 300 + }); + + // THEN + test.throws(() => { + new servicediscovery.Instance(stack, 'MyInstance', { + service, + instanceId: 'id', + instanceAttributes: {} + }); + }, /A `instanceCname` must be specified for a service using a `CNAME` record./); + + test.done(); + }, + + 'Throws when omitting port for a service using SRV'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'dns', + type: servicediscovery.NamespaceType.DnsPublic, + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + dnsRecordType: servicediscovery.DnsRecordType.Srv + }); + + // THEN + test.throws(() => { + new servicediscovery.Instance(stack, 'MyInstance', { + service, + instanceId: 'id', + instanceAttributes: {} + }); + }, /A `port` must be specified for a service using a `SRV` record./); + + test.done(); + }, + + 'Throws when omitting ipv4 and ipv6 for a service using SRV'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'dns', + type: servicediscovery.NamespaceType.DnsPublic, + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + dnsRecordType: servicediscovery.DnsRecordType.Srv + }); + + // THEN + test.throws(() => { + new servicediscovery.Instance(stack, 'MyInstance', { + service, + instanceId: 'id', + instanceAttributes: { + port: '3306' + } + }); + }, /At least `ipv4` or `ipv6` must be specified for a service using a `SRV` record./); + + test.done(); + }, + + 'Throws when omitting ipv4 for a service using A'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'dns', + type: servicediscovery.NamespaceType.DnsPublic + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + dnsRecordType: servicediscovery.DnsRecordType.A + }); + + // THEN + test.throws(() => { + new servicediscovery.Instance(stack, 'MyInstance', { + service, + instanceId: 'id', + instanceAttributes: { + port: '3306' + } + }); + }, /An `ipv4` must be specified for a service using a `A` record./); + + test.done(); + }, + + 'Throws when omitting ipv6 for a service using AAAA'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'dns', + type: servicediscovery.NamespaceType.DnsPublic + }); + + const service = new servicediscovery.Service(stack, 'MyService', { + namespace, + dnsRecordType: servicediscovery.DnsRecordType.AAAA + }); + + // THEN + test.throws(() => { + new servicediscovery.Instance(stack, 'MyInstance', { + service, + instanceId: 'id', + instanceAttributes: { + port: '3306' + } + }); + }, /An `ipv6` must be specified for a service using a `AAAA` record./); + + 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..099c5f5aa69ab --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/test.namespace.ts @@ -0,0 +1,71 @@ +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.Namespace(stack, 'MyNamespace', { + name: 'foobar.com', + type: servicediscovery.NamespaceType.Http, + }); + + 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.Namespace(stack, 'MyNamespace', { + name: 'foobar.com', + type: servicediscovery.NamespaceType.DnsPublic, + }); + + 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.Namespace(stack, 'MyNamespace', { + name: 'foobar.com', + type: servicediscovery.NamespaceType.DnsPrivate, + 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..108247a937d26 --- /dev/null +++ b/packages/@aws-cdk/aws-servicediscovery/test/test.service.ts @@ -0,0 +1,366 @@ +import { expect } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import servicediscovery = require('../lib'); +// import { DnsRecordType } from '../lib'; + +export = { + 'Service for HTTP namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'http', + type: servicediscovery.NamespaceType.Http, + }); + + new servicediscovery.Service(stack, 'MyService', { + namespace, + name: 'service', + description: 'service description', + healthCheckCustomConfig: { + failureThreshold: 3, + } + }); + + // THEN + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::HttpNamespace", + Properties: { + Name: "http" + }, + }, + MyServiceA1F951EB: { + Type: "AWS::ServiceDiscovery::Service", + Properties: { + Description: "service description", + HealthCheckCustomConfig: { + FailureThreshold: 3, + }, + Name: "service", + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + } + } + } + } + }); + + test.done(); + }, + + 'Service for DNS namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'dns', + type: servicediscovery.NamespaceType.DnsPublic, + }); + + new servicediscovery.Service(stack, 'MyService', { + namespace, + name: 'service', + description: 'service description', + healthCheckCustomConfig: { + failureThreshold: 3, + } + }); + + // THEN + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::PublicDnsNamespace", + Properties: { + Name: "dns" + } + }, + MyServiceA1F951EB: { + 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 DNS namespace with A and AAAA records'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'dns', + type: servicediscovery.NamespaceType.DnsPublic, + }); + + new servicediscovery.Service(stack, 'MyService', { + namespace, + dnsRecordType: servicediscovery.DnsRecordType.A_AAAA + }); + + // THEN + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::PublicDnsNamespace", + Properties: { + Name: "dns" + } + }, + MyServiceA1F951EB: { + Type: "AWS::ServiceDiscovery::Service", + Properties: { + DnsConfig: { + DnsRecords: [ + { + TTL: "60", + Type: "A, AAAA" + }, + ], + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + }, + RoutingPolicy: "MULTIVALUE", + }, + NamespaceId: { + "Fn::GetAtt": [ + "MyNamespaceD0BB8558", + "Id" + ] + } + } + } + } + }); + + test.done(); + }, + + 'Defaults to weighted for CNAME'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'dns', + type: servicediscovery.NamespaceType.DnsPublic, + }); + + new servicediscovery.Service(stack, 'MyService', { + namespace, + dnsRecordType: servicediscovery.DnsRecordType.Cname + }); + + // THEN + expect(stack).toMatch({ + Resources: { + MyNamespaceD0BB8558: { + Type: "AWS::ServiceDiscovery::PublicDnsNamespace", + Properties: { + Name: "dns" + } + }, + MyServiceA1F951EB: { + 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 routingPolicy for an HTTP only namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'name', + type: servicediscovery.NamespaceType.Http, + }); + + // THEN + test.throws(() => { + new servicediscovery.Service(stack, 'MyService', { + namespace, + name: 'service', + routingPolicy: servicediscovery.RoutingPolicy.Multivalue + }); + }, /`routingPolicy`/); + + test.done(); + }, + + 'Throws when specifying dnsRecord for an HTTP namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'name', + type: servicediscovery.NamespaceType.Http, + }); + + // THEN + test.throws(() => { + new servicediscovery.Service(stack, 'MyService', { + namespace, + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.A + }); + }, /`dnsRecord`/); + + test.done(); + }, + + 'Throws when specifying both healthCheckConfig and healthCheckCustomCOnfig'(test: Test) { + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'name', + type: servicediscovery.NamespaceType.Http, + }); + + // THEN + test.throws(() => { + new servicediscovery.Service(stack, 'MyService', { + namespace, + name: 'service', + healthCheckConfig: { + resourcePath: '/' + }, + healthCheckCustomConfig: { + failureThreshold: 1 + } + }); + }, /`healthCheckConfig`.+`healthCheckCustomConfig`/); + + test.done(); + }, + + 'Throws when specifying health check config for an Http Namespace'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'name', + type: servicediscovery.NamespaceType.Http, + }); + + // THEN + test.throws(() => { + new servicediscovery.Service(stack, 'MyService', { + namespace, + name: 'service', + healthCheckConfig: { + resourcePath: '/' + } + }); + }, /`healthCheckConfig`/); + + test.done(); + }, + + 'Throws when using CNAME and Multivalue'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'name', + type: servicediscovery.NamespaceType.DnsPublic, + }); + + // THEN + test.throws(() => { + new servicediscovery.Service(stack, 'MyService', { + namespace, + name: 'service', + dnsRecordType: servicediscovery.DnsRecordType.Cname, + routingPolicy: servicediscovery.RoutingPolicy.Multivalue, + }); + }, /`CNAME`.+`Multivalue`/); + + test.done(); + }, + + 'Throws when specifying resourcePath with TCP'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const namespace = new servicediscovery.Namespace(stack, 'MyNamespace', { + name: 'name', + type: servicediscovery.NamespaceType.DnsPublic, + }); + + // THEN + test.throws(() => { + new servicediscovery.Service(stack, 'MyService', { + namespace, + name: 'service', + healthCheckConfig: { + type: servicediscovery.HealthCheckType.Tcp, + resourcePath: '/check' + } + }); + }, /`resourcePath`.+`TCP`/); + + 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(); - } -});