From 6bc8cc4e1baa38e7ceb2a621e3001d1a98eb191f Mon Sep 17 00:00:00 2001 From: Jeremy Lecour Date: Tue, 22 Oct 2019 14:03:32 +0200 Subject: [PATCH] first version of the script --- .gitignore | 5 +- Gemfile | 11 +++ Gemfile.lock | 47 ++++++++++ aws-cert | 248 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100755 aws-cert diff --git a/.gitignore b/.gitignore index 8bccfed..24c5099 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ /tmp/ # Used by dotenv library to load environment variables. -# .env +.env ## Specific to RubyMotion: .dat* @@ -44,9 +44,8 @@ build-iPhoneSimulator/ # for a library or gem, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # Gemfile.lock -# .ruby-version +.ruby-version # .ruby-gemset # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc - diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..6a8a4a2 --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +gem "main" +gem "dotenv" +gem "aws-sdk-acm" +gem "aws-sdk-elasticloadbalancingv2" +gem "pry" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..d12d815 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,47 @@ +GEM + remote: https://rubygems.org/ + specs: + arrayfields (4.9.2) + aws-eventstream (1.0.3) + aws-partitions (1.226.0) + aws-sdk-acm (1.25.0) + aws-sdk-core (~> 3, >= 3.61.1) + aws-sigv4 (~> 1.1) + aws-sdk-core (3.69.1) + aws-eventstream (~> 1.0, >= 1.0.2) + aws-partitions (~> 1.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-elasticloadbalancingv2 (1.34.0) + aws-sdk-core (~> 3, >= 3.61.1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.1.0) + aws-eventstream (~> 1.0, >= 1.0.2) + chronic (0.10.2) + coderay (1.1.2) + dotenv (2.7.5) + fattr (2.4.0) + jmespath (1.4.0) + main (6.2.3) + arrayfields (~> 4.7, >= 4.7.4) + chronic (~> 0.6, >= 0.6.2) + fattr (~> 2.2, >= 2.2.0) + map (~> 6.1, >= 6.1.0) + map (6.6.0) + method_source (0.9.2) + pry (0.12.2) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + +PLATFORMS + ruby + +DEPENDENCIES + aws-sdk-acm + aws-sdk-elasticloadbalancingv2 + dotenv + main + pry + +BUNDLED WITH + 2.0.2 diff --git a/aws-cert b/aws-cert new file mode 100755 index 0000000..50642c7 --- /dev/null +++ b/aws-cert @@ -0,0 +1,248 @@ +#!/usr/bin/env ruby + +# The "main" gem is a framework for command line applications +require "main" +# load configuration from .env file +require 'dotenv/load' +# Import only the relevant AWS SDK +require 'aws-sdk-acm' +require 'aws-sdk-elasticloadbalancingv2' +# Useful for debugging +require "pry" + +class ACM + attr_reader :client + + def initialize(client) + @client = client + end + + # return the ARN of a certificate from its domain name + def certificate_arn_by_domain(domain_name) + cert_summary = client.list_certificates.certificate_summary_list.detect { |cert_summary| + cert_summary.domain_name == domain_name + } + cert_summary.certificate_arn + end + + # return the certificate_summary_list object from AWS sdk or yields if a block is given + def list_certificates(&block) + response = client.list_certificates + + if block_given? + response.certificate_summary_list.each do |cert_summary| + yield(cert_summary) + end + else + response.certificate_summary_list + end + end + + # return the certificate object from AWS sdk or yields if a block is given + def describe_certificate(certificate_arn_or_domain_name, &block) + certificate_arn = if acm_arn?(certificate_arn_or_domain_name) + certificate_arn_or_domain_name + else + certificate_arn_by_domain(certificate_arn_or_domain_name) + end + + response = client.describe_certificate(certificate_arn: certificate_arn) + certificate = response.certificate + + if block_given? + yield(certificate) + else + certificate + end + end + + # return the certificate ARN of the created certificate + def request_certificate(domain_name) + response = client.request_certificate({ + domain_name: domain_name, + validation_method: "DNS", + # idempotency_token: "IdempotencyToken", + }) + response.certificate_arn + end + + # return true/false if the input looks like an ACM ARN + def acm_arn?(input) + /\Aarn:aws:acm:/.match(input) + end + +end + +# print the certificate status (used in a yielded block) +def print_certificate_status(certificate) + puts "Domain name: #{certificate.domain_name}" + puts " ARN: #{certificate.certificate_arn}" + puts " Status: #{certificate.status}" + puts " Created at: #{certificate.created_at}" + puts " Not before: #{certificate.not_before}" + puts " Not after: #{certificate.not_after}" + puts " Issuer: #{certificate.issuer}" + puts " Renewable eligibility: #{certificate.renewal_eligibility}" + + if certificate.domain_validation_options + certificate.domain_validation_options.each do |validation_option| + puts " Validation option: #{validation_option.validation_method}" + puts " Status: #{validation_option.validation_status}" + case validation_option.validation_method + when "DNS" + if validation_option.resource_record + puts " Record name: #{validation_option.resource_record.name}" + puts " Record type: #{validation_option.resource_record.type}" + puts " Record value: #{validation_option.resource_record.value}" + else + puts " Record: _unavailable_" + end + when "EMAIL" + puts " Emails : #{validation_option.validation_emails}" + end + end + else + puts " Validation option: _unavailable_" + end +end + +# print the certificate summary (used in a yielded block) +def print_certificate_summary(cert_summary) + puts "Domain name: #{cert_summary.domain_name}" + puts " ARN: #{cert_summary.certificate_arn}" +end + +class ELBv2 + attr_reader :client + def initialize(client) + @client = client + end + + # return a boolean indicating if the cert has been attached to the listener + def add_listener_certificate(listener_arn, certificate_arn) + response = client.add_listener_certificates({ + listener_arn: listener_arn, + certificates: [ + { + certificate_arn: certificate_arn, + # is_default: false, + }, + ], + }) + + response.certificates.first.certificate_arn == certificate_arn + end + + # return the load_balancer object from AWS sdk or yields if a block is given + def describe_load_balancer(load_balancer_arn) + response = client.describe_load_balancers({ + load_balancer_arns: [ + load_balancer_arn + ], + }) + load_balancer = response.load_balancers.first + + if block_given? + yield(load_balancer) + else + load_balancer + end + end +end + +Main { + mode 'list' do + def run() + acm = ACM.new(acm_client) + acm.list_certificates do |cert_summary| + print_certificate_summary(cert_summary) + end + rescue Aws::Errors::ServiceError => ex + exit_status exit_failure + puts ex.message + end + end + + mode 'status' do + argument('domain') { required } + + def run() + domain_name = params[:domain].value + + acm = ACM.new(acm_client) + acm.describe_certificate(domain_name) do |certificate| + exit_status exit_failure unless certificate.status == "ISSUED" + print_certificate_status(certificate) + end + rescue Aws::Errors::ServiceError => ex + puts "#{ex.class.name}: #{ex.message}" + exit_failure! + end + end + + mode 'create' do + argument('domain') { required } + + def run() + domain_name = params[:domain].value + + acm = ACM.new(acm_client) + certificate_arn = acm.request_certificate(domain_name) + + acm.describe_certificate(certificate_arn) do |certificate| + exit_status exit_failure unless certificate.status == "ISSUED" + print_certificate_status(certificate) + end + rescue Aws::Errors::ServiceError => ex + puts "#{ex.class.name}: #{ex.message}" + exit_failure! + end + end + + mode 'enable' do + argument('domain') { required } + environment('ELB_ARN'){ required } + environment('ELB_LISTENER_ARN'){ required } + + def run() + domain_name = params['domain'].value + elb_arn = params['ELB_ARN'].value + elb_listener_arn = params['ELB_LISTENER_ARN'].value + + acm = ACM.new(acm_client) + certificate_arn = acm.certificate_arn_by_domain(domain_name) + certificate = acm.describe_certificate(certificate_arn) + + if certificate.status == "ISSUED" + elbv2 = ELBv2.new(elbv2_client) + elbv2.add_listener_certificate(elb_listener_arn, certificate_arn) + + elbv2.describe_load_balancer(elb_arn) do |load_balancer| + puts "Certificate has been added to load-balancer, change DNS configuration :" + puts " Record name: #{domain_name}." + puts " Record type: CNAME" + puts " Record value: #{load_balancer.dns_name}." + end + else + exit_status exit_failure + puts "Certificate for '#{domain_name}' is not available for ELB (status: #{certificate.status})" + end + rescue Aws::Errors::ServiceError => ex + puts "#{ex.class.name}: #{ex.message}" + exit_failure! + end + end + + def acm_client + args = {} + args[:profile] = ENV['AWS_PROFILE'] if ENV['AWS_PROFILE'] + # args[:logger] = Logger.new(STDOUT) + Aws::ACM::Client.new(args) + end + + def elbv2_client + args = {} + args[:profile] = ENV['AWS_PROFILE'] if ENV['AWS_PROFILE'] + Aws::ElasticLoadBalancingV2::Client.new(args) + end +}