Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/install certificate #48

Merged
merged 8 commits into from Jun 12, 2017
9 changes: 9 additions & 0 deletions .kitchen.yml
Expand Up @@ -39,3 +39,12 @@ suites:
access_token: <%= ENV['DNSIMPLE_ACCESS_TOKEN'] %>
base_url: https://api.sandbox.dnsimple.com
test_domain: dnsimple.xyz
- name: install_certificate
run_list:
- recipe[dnsimple_test::reset_test_environment]
- recipe[dnsimple_test::install_certificate]
attributes:
dnsimple:
access_token: <%= ENV['DNSIMPLE_ACCESS_TOKEN'] %>
base_url: https://api.sandbox.dnsimple.com
test_domain: dnsimple.xyz
8 changes: 6 additions & 2 deletions TESTING.md
Expand Up @@ -16,10 +16,14 @@ access token. You'll want to define it as an env var for test kitchen under
`DNSIMPLE_ACCESS_TOKEN=mytoken chef exec kitchen test`

You will also want to edit the `.kitchen.yml` file to adjust the `test_domain`
attributes to test a domain you create under your sandbox account. In order to
test regional records, you will want to upgrade the account plan to one that
attributes to test a domain you create under your sandbox account.

To test regional records, you will want to upgrade the account plan to one that
supports the regional records feature.

To test certificate installation, you will need to issue a certificate in sandbox
first.

[ChefDK]: https://downloads.chef.io/chef-dk/
[VirtualBox]: https://www.virtualbox.org/wiki/Downloads
[Vagrant]: https://www.vagrantup.com/downloads.html
Expand Down
79 changes: 79 additions & 0 deletions libraries/provider_dnsimple_certificate.rb
@@ -0,0 +1,79 @@
#
# Cookbook Name:: dnsimple
# Library:: provider_dnsimple_certificate
#
# Copyright 2014-2017 Aetrion, LLC dba DNSimple
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require_relative 'dnsimple_provider'

class Chef
class Provider
class DnsimpleCertificate < DnsimpleProvider
use_inline_resources

provides :dnsimple_certificate

def load_current_resource
@current_resource = Chef::Resource::DnsimpleCertificate.new(@new_resource.name)
@current_resource.certificate_common_name(@new_resource.certificate_common_name)
@current_resource.domain(@new_resource.domain)

# TODO: replace dnsimple_client.certificates.certificates with .all_certificates when it is added
# to the API client
certificates = dnsimple_client.certificates.certificates(dnsimple_client_account_id, @new_resource.domain)
@existing_certificate = certificates.data.detect do |certificate|
(certificate.common_name == @new_resource.certificate_common_name) && (certificate.state == 'issued') && (Date.parse(certificate.expires_on) > Date.today)
end

@current_resource.exists = !@existing_certificate.nil?
# rubocop:disable Style/GuardClause
if @current_resource.exists
@existing_certificate_bundle = dnsimple_client.certificates.download_certificate(dnsimple_client_account_id, @new_resource.domain, @existing_certificate.id).data
@existing_private_key = dnsimple_client.certificates.certificate_private_key(dnsimple_client_account_id, @new_resource.domain, @existing_certificate.id).data
@current_resource.expires_on = Date.parse(@existing_certificate.expires_on)
@current_resource.server_pem = @existing_certificate_bundle.server
@current_resource.chain_pem = @existing_certificate_bundle.chain
@current_resource.private_key_pem = @existing_private_key.private_key
end
end

action :install do
if @current_resource.exists
install_certificate
else
Chef::Log.info "DNSimple: no certificate found #{new_resource.certificate_common_name}"
end
end

def install_certificate
converge_by("install certificate #{current_resource.certificate_common_name} expiring #{current_resource.expires_on}") do
declare_resource(:file, "#{current_resource.name}/#{current_resource.domain}.crt") do
content "#{current_resource.server_pem}#{current_resource.chain_pem.join("\n")}"
mode current_resource.mode
owner current_resource.owner
group current_resource.group
end
declare_resource(:file, "#{current_resource.name}/#{current_resource.domain}.key") do
content current_resource.private_key_pem
mode current_resource.mode
owner current_resource.owner
group current_resource.group
sensitive true
end
end
end
end
end
end
42 changes: 42 additions & 0 deletions libraries/resource_dnsimple_certificate.rb
@@ -0,0 +1,42 @@
#
# Cookbook Name:: dnsimple
# Library:: resource_dnsimple_certificate
#
# Copyright 2014-2017 Aetrion, LLC dba DNSimple
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require_relative 'dnsimple_resource'

class Chef
class Resource
class DnsimpleCertificate < DnsimpleResource
resource_name :dnsimple_certificate

allowed_actions :install
default_action :install

property :install_path, kind_of: String, name_property: true
property :certificate_common_name, kind_of: String, required: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we key on common name as part of the existence check, should that also be the name property?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not the impression I got from @onlyhavecans - he indicated that since this is effectively a file resource that the file path should be the name property.

property :domain, kind_of: String, required: true
property :expires_on, kind_of: Date
property :server_pem, kind_of: String
property :chain_pem, kind_of: Array
property :private_key_pem, kind_of: String

property :mode, kind_of: String
property :owner, kind_of: String
property :group, kind_of: String
end
end
end
56 changes: 56 additions & 0 deletions spec/libraries/provider_dnsimple_certificate_spec.rb
@@ -0,0 +1,56 @@
require 'spec_helper'
require 'dnsimple'
require_relative '../../libraries/provider_dnsimple_certificate'
require_relative '../../libraries/resource_dnsimple_certificate'

describe Chef::Provider::DnsimpleCertificate do
before(:each) do
@node = stub_node(platform: 'ubuntu', version: '14.04')
@events = Chef::EventDispatch::Dispatcher.new
@new_resource = Chef::Resource::DnsimpleCertificate.new('/path/to/certificate.crt')
@run_context = Chef::RunContext.new(@node, {}, @events)
@current_resource = Chef::Resource::DnsimpleCertificate.new('/path/to/certificate.crt')
@provider = Chef::Provider::DnsimpleCertificate.new(@new_resource, @run_context)
end

describe '#install' do
before(:each) do
@new_resource.access_token('this_is_a_token')
@provider.dnsimple_client = client
@new_resource.certificate_common_name = certificate_data[:common_name]
@new_resource.domain = 'example.com'
@provider.current_resource = @current_resource
end

let(:client) { instance_double(Dnsimple::Client, identity: identity, certificates: certificates) }
let(:identity) { instance_double(Dnsimple::Client::Identity, whoami: whoami_response) }
let(:whoami_response) { instance_double(Dnsimple::Response, data: data) }
let(:data) { instance_double(Dnsimple::Struct::Whoami, account: account) }
let(:account) { instance_double(Dnsimple::Struct::Account, id: 1) }
let(:certificates) { instance_double(Dnsimple::Client::Certificates, certificates: certificate_list, download_certificate: certificate_bundle_response, certificate_private_key: private_key_bundle_response) }
let(:certificate_list) { instance_double(Dnsimple::CollectionResponse, data: [certificate]) }
let(:certificate) { instance_double(Dnsimple::Struct::Certificate, id: certificate_data[:id], common_name: certificate_data[:common_name], expires_on: certificate_data[:expires_on], state: 'issued') }
let(:certificate_bundle_response) { instance_double(Dnsimple::Response, data: certificate_bundle) }
let(:certificate_bundle) { instance_double(Dnsimple::Struct::CertificateBundle, server: 'server-pem', chain: ['chain-pem']) }
let(:private_key_bundle_response) { instance_double(Dnsimple::Response, data: private_key_bundle) }
let(:private_key_bundle) { instance_double(Dnsimple::Struct::CertificateBundle, private_key: 'private-key-pem') }
let(:certificate_data) do
{
id: 1,
common_name: 'www.example.com',
expires_on: '2018-01-01',
}
end

context 'if the certificate exists' do
it 'installs the certificate' do
@provider.run_action(:install)
expect(@new_resource).to be_updated
end
end

it 'implements the load_current_resource interface' do
expect { @provider.load_current_resource }.to_not raise_exception
end
end
end
15 changes: 15 additions & 0 deletions test/cookbooks/dnsimple_test/recipes/install_certificate.rb
@@ -0,0 +1,15 @@
directory '/etc/apache2/ssl' do
owner 'www-data'
group 'www-data'
recursive true
end

dnsimple_certificate '/etc/apache2/ssl' do
certificate_common_name 'www.dnsimple.xyz'
domain node['dnsimple']['test_domain']
access_token node['dnsimple']['access_token']
base_url node['dnsimple']['base_url']
mode '0755'
owner 'web_admin'
group 'web_admin'
end
@@ -0,0 +1,8 @@
# TODO: verify with x509_certificate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these for some other day in the future or this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be for now or later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you know the command it's as easy as

describe command('my cool ssl check command') do
  its('stdout') { should match /totally right/ }
  its('exit_code') { should eq 0 }
end

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the folks who helped my at the hackday indicated there are full blown test helpers for public and private keys in inspec that remove the need for match. At least that's what I understood.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

describe file('/etc/apache2/ssl/dnsimple.xyz.crt') do
its('content') { should match /-----BEGIN CERTIFICATE-----/ }
end
# TODO: verify with key_rsa
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these for some other day in the future or this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be for now or later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.inspec.io/docs/reference/resources/key_rsa/ seems simple to impliment I think and could be put in here

describe file('/etc/apache2/ssl/dnsimple.xyz.key') do
its('content') { should match /-----BEGIN RSA PRIVATE KEY-----/ }
end
11 changes: 11 additions & 0 deletions test/integration/install_certificate/inspec.yml
@@ -0,0 +1,11 @@
name: install_certificate
title: Install Certificate Test
maintainer: Aetrion, LLC dba DNSimple
copyright: Aetrion, LLC dba DNSimple
copyright_email: ops@dnsimple.com
license: Apache 2 license
summary: Confirms the test cookbook to install a certificate via the sandbox API
version: 1.0.0
supports:
- os-family: linux