diff --git a/Readme.md b/Readme.md index 2a95915..f27df7a 100644 --- a/Readme.md +++ b/Readme.md @@ -43,7 +43,7 @@ Check out their getting started guide for more information [here](https://server Make sure you have the following installed before starting: * [nodejs](https://nodejs.org/en/download/) * [npm](https://www.npmjs.com/get-npm?utm_source=house&utm_medium=homepage&utm_campaign=free%20orgs&utm_term=Install%20npm) -* [serverless](https://serverless.com/framework/docs/providers/aws/guide/installation/) +* [serverless](https://serverless.com/framework/docs/providers/aws/guide/installation/) >= v1.52.0 # Usage @@ -62,11 +62,11 @@ open serverless.yml and add the following: certificateName: 'abc.somedomain.io' //optional idempotencyToken: 'abcsomedomainio' - //required if hostedZoneId is not set - hostedZoneName: 'somedomain.io.' - //required if hostedZoneName is not set - hostedZoneId: 'XXXXXXXXX' - // optional default is false. if you set it to true you will get a new file (after executing serverless create-cert), that contains certificate info that you can use in your deploy pipeline + //required if hostedZoneIds is not set, alternativly as an array + hostedZoneNames: 'somedomain.io.' + //required if hostedZoneNames is not set + hostedZoneIds: 'XXXXXXXXX' + // optional default is false. if you set it to true you will get a new file (after executing serverless create-cert), that contains certificate info that you can use in your deploy pipeline, alternativly as an array writeCertInfoToFile: false // optional, only used when writeCertInfoToFile is set to true. It sets the name of the file containing the cert info certInfoFileName: 'cert-info.yml' @@ -81,6 +81,8 @@ open serverless.yml and add the following: tags: Name: 'somedomain.com' Environment: 'prod' + //optional default false. this is useful if you managed to delete your certificate but the dns validation records still exist + rewriteRecords: false now you can run: @@ -118,10 +120,11 @@ Open serverless.yml and add the following: customCertificate: certificateName: 'abc.somedomain.io' //required idempotencyToken: 'abcsomedomainio' //optional - hostedZoneName: 'somedomain.io.' //required if hostedZoneId is not set - hostedZoneId: 'XXXXXXXXX' //required if hostedZoneName is not set + hostedZoneNames: 'somedomain.io.' //required if hostedZoneIds is not set + hostedZoneIds: 'XXXXXXXXX' //required if hostedZoneNames is not set region: eu-west-1 // optional - default is us-east-1 which is required for custom api gateway domains of Type Edge (default) enabled: true // optional - default is true. For some stages you may not want to use certificates (and custom domains associated with it). + rewriteRecords: false Now you can run: @@ -130,6 +133,14 @@ Now you can run: Please make sure to check out the complete sample project [here](https://github.com/schwamster/serverless-certificate-creator/tree/master/examples/certificate-creator-example). +### Reference Certificate Arn via variableResolvers + +Since version 1.2.0 of this plugin you can use the following syntax to access the certificates Arn in other plugins + + ${certificate:${self:custom.customCertificate.certificateName}:CertificateArn} + +see the serverless [docs](https://serverless.com/framework/docs/providers/aws/guide/plugins#custom-variable-types) for more information + ### License Copyright (c) 2018 Bastian Töpfer, contributors. diff --git a/examples/certificate-creator-example/package.json b/examples/certificate-creator-example/package.json index 3d21932..c666979 100644 --- a/examples/certificate-creator-example/package.json +++ b/examples/certificate-creator-example/package.json @@ -9,7 +9,7 @@ "author": "", "license": "ISC", "devDependencies": { - "serverless-certificate-creator": "^1.0.8", + "serverless-certificate-creator": "^1.2.0", "serverless-domain-manager": "^3.2.6" } } diff --git a/examples/certificate-creator-example/serverless.yml b/examples/certificate-creator-example/serverless.yml index e6b5cce..89bf525 100644 --- a/examples/certificate-creator-example/serverless.yml +++ b/examples/certificate-creator-example/serverless.yml @@ -42,10 +42,11 @@ custom: idempotencyToken: 'certcreatorsamplegreenelephantio' writeCertInfoToFile: true certInfoFileName: "certs/${self:provider.stage}/cert-info.yml" - hostedZoneName: 'greenelephant.io.' + hostedZoneNames: 'greenelephant.io.' subjectAlternativeNames : - 'certcreatorsample1.greenelephant.io' - 'certcreatorsample2.greenelephant.io' tags: Name: 'somedomain.com' - Environment: 'prod' \ No newline at end of file + Environment: 'prod' + rewriteRecords: false \ No newline at end of file diff --git a/index.js b/index.js index 385a93f..3fe34e0 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ const fs = require('fs'); const path = require('path'); const YAML = require('yamljs'); const mkdirp = require('mkdirp'); +var packageJson = require('./package.json'); const unsupportedRegionPrefixes = ['cn-']; @@ -13,7 +14,7 @@ class CreateCertificatePlugin { this.serverless = serverless; this.options = options; this.initialized = false; - + this.serverless.cli.log(`serverless-certificate-creator version ${packageJson.version} called`); this.commands = { 'create-cert': { usage: 'creates a certificate for an existing domain/hosted zone', @@ -28,6 +29,14 @@ class CreateCertificatePlugin { 'after:deploy:deploy': this.certificateSummary.bind(this), 'after:info:info': this.certificateSummary.bind(this), }; + + this.variableResolvers = { + certificate: { + resolver: this.getCertificateProperty.bind(this), + isDisabledAtPrepopulation: true, + serviceName: 'serverless-certificate-creator depends on AWS credentials.' + } + }; } initializeVariables() { @@ -38,12 +47,15 @@ class CreateCertificatePlugin { this.route53 = new this.serverless.providers.aws.sdk.Route53(credentials); this.region = this.serverless.service.custom.customCertificate.region || 'us-east-1'; this.domain = this.serverless.service.custom.customCertificate.certificateName; - this.hostedZoneId = this.serverless.service.custom.customCertificate.hostedZoneId; - this.hostedZoneName = this.serverless.service.custom.customCertificate.hostedZoneName; + //hostedZoneId is mapped for backwards compatibility + this.hostedZoneIds = this.serverless.service.custom.customCertificate.hostedZoneIds ? this.serverless.service.custom.customCertificate.hostedZoneIds : (this.serverless.service.custom.customCertificate.hostedZoneId) ? [].concat(this.serverless.service.custom.customCertificate.hostedZoneId) : []; + //hostedZoneName is mapped for backwards compatibility + this.hostedZoneNames = this.serverless.service.custom.customCertificate.hostedZoneNames ? this.serverless.service.custom.customCertificate.hostedZoneNames : (this.serverless.service.custom.customCertificate.hostedZoneName) ? [].concat(this.serverless.service.custom.customCertificate.hostedZoneName) : []; const acmCredentials = Object.assign({}, credentials, { region: this.region }); this.acm = new this.serverless.providers.aws.sdk.ACM(acmCredentials); this.idempotencyToken = this.serverless.service.custom.customCertificate.idempotencyToken; this.writeCertInfoToFile = this.serverless.service.custom.customCertificate.writeCertInfoToFile || false; + this.rewriteRecords = this.serverless.service.custom.customCertificate.rewriteRecords || false; this.certInfoFileName = this.serverless.service.custom.customCertificate.certInfoFileName || 'cert-info.yml'; this.subjectAlternativeNames = this.serverless.service.custom.customCertificate.subjectAlternativeNames || []; this.tags = this.serverless.service.custom.customCertificate.tags || {}; @@ -55,7 +67,6 @@ class CreateCertificatePlugin { } }) } - this.initialized = true; } } @@ -110,7 +121,7 @@ class CreateCertificatePlugin { CertificateArn: certificateArn, Tags: mappedTags } - + this.serverless.cli.log(`tagging certificate`); return this.acm.addTagsToCertificate(params).promise().catch(error => { this.serverless.cli.log('tagging certificate failed', error); @@ -228,21 +239,19 @@ class CreateCertificatePlugin { }); } - getHostedZoneId() { + getHostedZoneIds() { return this.route53.listHostedZones({}).promise().then(data => { - if (this.hostedZoneId) { - return this.hostedZoneId; - } + let hostedZones = data.HostedZones.filter(x => this.hostedZoneIds.includes(x.Id.replace(/\/hostedzone\//g, '')) || this.hostedZoneNames.includes(x.Name)); - let hostedZone = data.HostedZones.filter(x => x.Name == this.hostedZoneName); - if (hostedZone.length == 0) { + if (hostedZones.length == 0) { throw "no hosted zone for domain found" } - this.hostedZoneId = hostedZone[0].Id.replace(/\/hostedzone\//g, ''); - return this.hostedZoneId; + return hostedZones.map(({ Id, Name }) => { + return { hostedZoneId: Id.replace(/\/hostedzone\//g, ''), Name: Name.substr(0, Name.length - 1) }; + }); }).catch(error => { this.serverless.cli.log('certificate validation failed', error); console.log('problem', error); @@ -255,38 +264,40 @@ class CreateCertificatePlugin { * at least a short time after the cert has been created, thats why you should delay this call a bit after u created a new cert */ createRecordSetForDnsValidation(certificate) { - return this.getHostedZoneId().then((hostedZoneId) => { - - let changes = certificate.Certificate.DomainValidationOptions.map((x) => { - return { - Action: "CREATE", - ResourceRecordSet: { - Name: x.ResourceRecord.Name, - ResourceRecords: [ - { - Value: x.ResourceRecord.Value - } - ], - TTL: 60, - Type: x.ResourceRecord.Type + return this.getHostedZoneIds().then((hostedZoneIds) => { + + return Promise.all(hostedZoneIds.map(({ hostedZoneId, Name }) => { + let changes = certificate.Certificate.DomainValidationOptions.filter(({DomainName}) => DomainName.endsWith(Name)).map((x) => { + return { + Action: this.rewriteRecords ? "UPSERT" : "CREATE", + ResourceRecordSet: { + Name: x.ResourceRecord.Name, + ResourceRecords: [ + { + Value: x.ResourceRecord.Value + } + ], + TTL: 60, + Type: x.ResourceRecord.Type + } } - } - }); + }); - var params = { - ChangeBatch: { - Changes: changes, - Comment: `DNS Validation for certificate ${certificate.Certificate.DomainValidationOptions[0].DomainName}` - }, - HostedZoneId: hostedZoneId - }; - return this.route53.changeResourceRecordSets(params).promise().then(recordSetResult => { - this.serverless.cli.log('dns validation record(s) created - certificate is ready for use after validation has gone through'); - }).catch(error => { - this.serverless.cli.log('could not create record set for dns validation', error); - console.log('problem', error); - throw error; - }); + var params = { + ChangeBatch: { + Changes: changes, + Comment: `DNS Validation for certificate ${Name}` + }, + HostedZoneId: hostedZoneId + }; + return this.route53.changeResourceRecordSets(params).promise().then(recordSetResult => { + this.serverless.cli.log('dns validation record(s) created - certificate is ready for use after validation has gone through'); + }).catch(error => { + this.serverless.cli.log('could not create record set for dns validation', error); + console.log('problem', error); + throw error; + }); + })); }); } @@ -306,6 +317,26 @@ class CreateCertificatePlugin { return true; }); } + + getCertificateProperty(src) { + this.initializeVariables(); + let [s, domainName, property] = src.split(':'); + return this.listCertificates() + .then(({ CertificateSummaryList }) => { + let cert = CertificateSummaryList.filter(({ DomainName }) => DomainName == domainName)[0]; + if (cert && cert[property]) { + return cert[property]; + } else { + this.serverless.cli.consoleLog(chalk.yellow('Warning, certificate or certificate property was not found. Returning an empty string instead!')); + return ''; + } + }) + .catch(error => { + console.log(this.domain, this.region); + this.serverless.cli.log('Could not find certificate property attempting to create...'); + throw error; + }); + } } diff --git a/package.json b/package.json index 8aef51d..2cfc894 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-certificate-creator", - "version": "1.1.0", + "version": "1.2.0", "description": "creates a certificate that can be used for custom domains for your api gateway", "main": "index.js", "scripts": {