├── .gitattributes
├── Berksfile
├── .delivery
├── build_cookbook
│ ├── recipes
│ │ ├── lint.rb
│ │ ├── unit.rb
│ │ ├── default.rb
│ │ ├── publish.rb
│ │ ├── quality.rb
│ │ ├── smoke.rb
│ │ ├── syntax.rb
│ │ ├── provision.rb
│ │ ├── security.rb
│ │ ├── functional.rb
│ │ └── deploy.rb
│ ├── metadata.rb
│ └── Berksfile
├── project.toml
└── config.json
├── CODE_OF_CONDUCT.md
├── TESTING.md
├── .vscode
└── extensions.json
├── CHANGELOG.md
├── .editorconfig
├── .github
└── workflows
│ └── branchcleanup.yml
├── metadata.rb
├── spec
├── unit
│ ├── recipes
│ │ ├── default_spec.rb
│ │ ├── provision_spec.rb
│ │ ├── unit_spec.rb
│ │ ├── lint_spec.rb
│ │ ├── deploy_spec.rb
│ │ ├── syntax_spec.rb
│ │ ├── functional_spec.rb
│ │ └── publish_spec.rb
│ └── libraries
│ │ ├── helpers_deploy_spec.rb
│ │ ├── helpers_functional_spec.rb
│ │ ├── helpers_publish_spec.rb
│ │ ├── delivery_api_client_specs.rb
│ │ ├── helpers_syntax_spec.rb
│ │ ├── helpers_lint_spec.rb
│ │ └── helpers_provision_spec.rb
└── spec_helper.rb
├── .gitignore
├── recipes
├── functional.rb
├── quality.rb
├── security.rb
├── smoke.rb
├── unit.rb
├── provision.rb
├── deploy.rb
├── default.rb
├── lint.rb
├── syntax.rb
└── publish.rb
├── libraries
├── matchers.rb
├── dsl.rb
├── helpers_unit.rb
├── helpers_functional.rb
├── helpers_deploy.rb
├── delivery_api_client.rb
├── helpers_syntax.rb
├── helpers_publish.rb
├── helpers_lint.rb
├── delivery_truck_deploy.rb
└── helpers_provision.rb
├── chefignore
├── README.md
└── LICENSE
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/Berksfile:
--------------------------------------------------------------------------------
1 | source 'https://supermarket.chef.io'
2 |
3 | metadata
4 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/lint.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::lint'
2 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/unit.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::unit'
2 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/default.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::default'
2 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/publish.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::publish'
2 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/quality.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::quality'
2 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/smoke.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::smoke'
2 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/syntax.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::syntax'
2 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/provision.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::provision'
2 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/security.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::security'
2 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/functional.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::functional'
2 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | Please refer to the Chef Community Code of Conduct at
2 |
--------------------------------------------------------------------------------
/TESTING.md:
--------------------------------------------------------------------------------
1 | Please refer to
2 |
3 |
--------------------------------------------------------------------------------
/.delivery/project.toml:
--------------------------------------------------------------------------------
1 | remote_file = "https://raw.githubusercontent.com/chef-cookbooks/community_cookbook_tools/master/delivery/project.toml"
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "chef-software.chef",
4 | "rebornix.ruby",
5 | "editorconfig.editorconfig"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.delivery/build_cookbook/metadata.rb:
--------------------------------------------------------------------------------
1 | name 'delivery-truck-build-cookbook'
2 | maintainer 'Chef Delivery Team'
3 | maintainer_email 'delivery-team@chef.io'
4 | license 'Apache 2.0'
5 | description 'Build the delivery-truck cookbook'
6 | version '0.1.0'
7 |
8 | depends 'delivery-truck'
9 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # delivery-truck Cookbook CHANGELOG
2 |
3 | This file is used to list changes made in each version of the delivery-truck cookbook.
4 |
5 | # 2.4.0 (2019-08-29)
6 |
7 | - Update the license string in the metadata.rb to a SPDX compliant license string
8 | - Add resources to bumped version helper
9 | - Add retry to push job during deploy phase
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root=true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 |
11 | # 2 space indentation
12 | indent_style = space
13 | indent_size = 2
14 |
15 | # Avoid issues parsing cookbook files later
16 | charset = utf-8
17 |
18 | # Avoid cookstyle warnings
19 | trim_trailing_whitespace = true
20 |
--------------------------------------------------------------------------------
/.github/workflows/branchcleanup.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Branch Cleanup
3 | # This workflow is triggered on all closed pull requests.
4 | # However the script does not do anything if a merge was not performed.
5 | "on":
6 | pull_request:
7 | types: [closed]
8 |
9 | env:
10 | NO_BRANCH_DELETED_EXIT_CODE: 0
11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: jessfraz/branch-cleanup-action@master
18 |
--------------------------------------------------------------------------------
/metadata.rb:
--------------------------------------------------------------------------------
1 | name 'delivery-truck'
2 | maintainer 'Chef Delivery Team'
3 | maintainer_email 'delivery-team@chef.io'
4 | license 'Apache-2.0'
5 | description 'Delivery build_cookbook for your cookbooks!'
6 | version '2.4.0'
7 |
8 | source_url 'https://github.com/chef-cookbooks/delivery-truck'
9 | issues_url 'https://github.com/chef-cookbooks/delivery-truck/issues'
10 |
11 | supports 'ubuntu', '>= 12.04'
12 | supports 'redhat', '>= 6.5'
13 | supports 'centos', '>= 6.5'
14 |
15 | depends 'delivery-sugar', '~> 1.1'
16 |
--------------------------------------------------------------------------------
/spec/unit/recipes/default_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "delivery-truck::default", :ignore => true do
4 | let(:chef_run) { ChefSpec::SoloRunner.converge(described_recipe) }
5 |
6 | it 'should install chefspec' do
7 | expect(chef_run).to install_chef_gem('chefspec')
8 | .with_version('4.1.1')
9 | .with_compile_time(false)
10 | end
11 |
12 | it 'should upgrade chef-sugar' do
13 | expect(chef_run).to upgrade_chef_gem('chef-sugar')
14 | .with_compile_time(false)
15 | end
16 |
17 | it 'should install knife-supermarket' do
18 | expect(chef_run).to install_chef_gem('chefspec')
19 | .with_compile_time(false)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.rbc
2 | .config
3 | InstalledFiles
4 | lib/bundler/man
5 | pkg
6 | test/tmp
7 | test/version_tmp
8 | tmp
9 | _Store
10 | *~
11 | *#
12 | .#*
13 | \#*#
14 | *.un~
15 | *.tmp
16 | *.bk
17 | *.bkup
18 |
19 | # editor temp files
20 | .idea
21 | .*.sw[a-z]
22 |
23 | # ruby/bundler files
24 | .ruby-version
25 | .ruby-gemset
26 | .rvmrc
27 | Gemfile.lock
28 | .bundle
29 | *.gem
30 | coverage
31 | spec/reports
32 |
33 | # YARD / rdoc artifacts
34 | .yardoc
35 | _yardoc
36 | doc/
37 | rdoc
38 |
39 | # chef infra stuff
40 | Berksfile.lock
41 | .kitchen
42 | kitchen.local.yml
43 | vendor/
44 | .coverage/
45 | .zero-knife.rb
46 | Policyfile.lock.json
47 |
48 | # vagrant stuff
49 | .vagrant/
50 | .vagrant.d/
51 |
--------------------------------------------------------------------------------
/.delivery/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2",
3 | "build_cookbook": {
4 | "name": "delivery-truck-build-cookbook",
5 | "path": ".delivery/build_cookbook"
6 | },
7 | "job_dispatch": {
8 | "version": "v2"
9 | },
10 | "delivery-truck": {
11 | "publish": {
12 | "chef_server": true,
13 | "github": "chef-cookbooks/delivery-truck",
14 | "supermarket": "https://supermarket.chef.io",
15 | "supermarket-custom-credentials": true
16 | },
17 | "lint": {
18 | "foodcritic": {
19 | "ignore_rules": ["FC009", "FC057", "FC058"]
20 | }
21 | }
22 | },
23 | "dependencies": []
24 | }
25 |
--------------------------------------------------------------------------------
/recipes/functional.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # SKIP PHASE
19 |
--------------------------------------------------------------------------------
/recipes/quality.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # This recipe is intentionally left blank
19 |
--------------------------------------------------------------------------------
/recipes/security.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # This recipe is intentionally left blank
19 |
--------------------------------------------------------------------------------
/recipes/smoke.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # This recipe is intentionally left blank
19 |
--------------------------------------------------------------------------------
/spec/unit/libraries/helpers_deploy_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DeliveryTruck::Helpers::Deploy do
4 | let(:node) { Chef::Node.new }
5 |
6 | describe '.deployment_search_query' do
7 | context 'when config value is unset' do
8 | it 'returns default search query' do
9 | expect(described_class.deployment_search_query(node)).to eql('recipes:*push-jobs*')
10 | end
11 | end
12 |
13 | context 'when config value is set' do
14 | let(:custom_search){ 'cool:attributes OR awful:constraints' }
15 | it 'returns the custom search query' do
16 | node.default['delivery']['config']['delivery-truck']['deploy']['search'] = custom_search
17 | expect(described_class.deployment_search_query(node)).to eql(custom_search)
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/Berksfile:
--------------------------------------------------------------------------------
1 | source 'https://supermarket.chef.io'
2 |
3 | metadata
4 |
5 | def whoami
6 | Etc.getpwuid(Process.uid).name
7 | end
8 |
9 | # If we are running inside Delivery
10 | if whoami.eql?('dbuild')
11 | # Consume delivery-sugar early on from DCC so we can detect
12 | # any failure before we release it to Supermarket
13 | cookbook 'delivery-sugar',
14 | git: 'ssh://builder@chef@delivery.chef.co:8989/chef/Delivery-Build-Cookbooks/delivery-sugar',
15 | branch: 'master'
16 | end
17 |
18 | # In order for the Delivery CLI to properly find this cookbook we need
19 | # to specify the path in the scope of the Delivery CLI's workspace.
20 | # This file is located (and run) from `./chef/build_cookbook` but delivery-truck
21 | # is located at `./repo`
22 | cookbook 'delivery-truck', path: File.expand_path('../../repo')
23 |
--------------------------------------------------------------------------------
/spec/unit/libraries/helpers_functional_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DeliveryTruck::Helpers::Functional do
4 |
5 | describe '.has_kitchen_tests?' do
6 | context 'when .kitchen.docker.yml file is present' do
7 | before do
8 | allow(File).to receive(:exist?).with('/tmp/cookbook/.kitchen.docker.yml').and_return(true)
9 | end
10 |
11 | it 'returns true' do
12 | expect(DeliveryTruck::Helpers::Functional.has_kitchen_tests?('/tmp/cookbook')).to eql true
13 | end
14 | end
15 |
16 | context 'when .kitchen.docker.yml file is missing' do
17 | before do
18 | allow(File).to receive(:exist?).with('/tmp/cookbook/.kitchen.docker.yml').and_return(false)
19 | end
20 |
21 | it 'returns false' do
22 | expect(DeliveryTruck::Helpers::Functional.has_kitchen_tests?('/tmp/cookbook')).to eql false
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/recipes/unit.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | changed_cookbooks.each do |cookbook|
19 | # Run RSpec against the modified cookbook
20 | execute "unit_rspec_#{cookbook.name}" do
21 | cwd cookbook.path
22 | command 'rspec --format documentation --color'
23 | only_if { has_spec_tests?(cookbook.path) }
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/libraries/matchers.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | if defined?(ChefSpec)
19 | def create_chef_environment(resource_name)
20 | ChefSpec::Matchers::ResourceMatcher.new(:chef_environment, :create, resource_name)
21 | end
22 |
23 | def run_delivery_truck_deploy(resource_name)
24 | ChefSpec::Matchers::ResourceMatcher.new(:delivery_truck_deploy, :run, resource_name)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/recipes/provision.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # TODO: This is a temporary workaround; ultimately, this should be
19 | # handled either by delivery_build or (preferably) the server itself.
20 | ruby_block 'copy env from prior to current' do
21 | block do
22 | with_server_config do
23 | stage_name = node['delivery']['change']['stage']
24 |
25 | ::DeliveryTruck::Helpers::Provision.provision(stage_name, node, get_acceptance_environment,
26 | get_all_project_cookbooks)
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/libraries/dsl.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # These files create / add to the Delivery::DSL module
19 | require_relative 'helpers_functional'
20 | require_relative 'helpers_lint'
21 | require_relative 'helpers_unit'
22 | require_relative 'helpers_publish'
23 | require_relative 'helpers_syntax'
24 | require_relative 'helpers_deploy'
25 |
26 | # And these mix the DSL methods into the Chef infrastructure
27 | Chef::Recipe.send(:include, DeliveryTruck::DSL)
28 | Chef::Resource.send(:include, DeliveryTruck::DSL)
29 | Chef::Provider.send(:include, DeliveryTruck::DSL)
30 |
--------------------------------------------------------------------------------
/libraries/helpers_unit.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | module DeliveryTruck
19 | module Helpers
20 | module Unit
21 | extend self
22 |
23 | # Look in the cookbook and return whether or not we can find Spec tests.
24 | #
25 | # @param cookbook_path [String] Path to cookbook
26 | # @return [TrueClass, FalseClass]
27 | def has_spec_tests?(cookbook_path)
28 | File.directory?(File.join(cookbook_path, 'spec'))
29 | end
30 | end
31 | end
32 |
33 | module DSL
34 | # Does cookbook have spec tests?
35 | def has_spec_tests?(cookbook_path)
36 | DeliveryTruck::Helpers::Unit.has_spec_tests?(cookbook_path)
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/libraries/helpers_functional.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | module DeliveryTruck
19 | module Helpers
20 | module Functional
21 | extend self
22 |
23 | # Look in the cookbook and return whether or not we can find a .kitchen.yml
24 | #
25 | # @param cookbook_path [String] Path to cookbook
26 | # @return [TrueClass, FalseClass]
27 | def has_kitchen_tests?(cookbook_path)
28 | File.exist?(File.join(cookbook_path, '.kitchen.docker.yml'))
29 | end
30 | end
31 | end
32 |
33 | module DSL
34 | # Can we find Test Kitchen files?
35 | def has_kitchen_tests?(cookbook_path)
36 | DeliveryTruck::Helpers::Functional.has_kitchen_tests?(cookbook_path)
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/recipes/deploy.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # Send CCR requests to every node that is running this cookbook or any
19 | # other one in the current project
20 | search_terms = []
21 | get_all_project_cookbooks.each do |cookbook|
22 | search_terms << "recipes:#{cookbook.name}*"
23 | end
24 |
25 | unless search_terms.empty?
26 | search_query = "(#{search_terms.join(' OR ')}) " \
27 | "AND chef_environment:#{delivery_environment} " \
28 | "AND #{deployment_search_query}"
29 |
30 | log "Search criteria used to deploy: '#{search_query}'"
31 |
32 | my_nodes = delivery_chef_server_search(:node, search_query)
33 | my_nodes.map!(&:name)
34 |
35 | delivery_push_job "deploy_#{node['delivery']['change']['project']}" do
36 | command 'chef-client'
37 | nodes my_nodes
38 | retries 5
39 | retry_delay 10
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/recipes/default.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # Everything we need comes with chef-dk
19 | #
20 | # If the end-user is still using chef-dk 0.12 we would need to install
21 | # knife-supermarket gem since we made it part of DK in version 0.13
22 | #
23 | # Notify the user that they need to upgrade to the latest chef-dk since
24 | # we don't want to install gems that we already ship within DK.
25 | #
26 | # TODO: Remove this in Stage 2
27 | chef_gem 'knife-supermarket' do
28 | compile_time false
29 | only_if do
30 | require 'chef-dk/version'
31 | Gem::Version.new(::ChefDK::VERSION) < Gem::Version.new('0.14')
32 | end
33 | only_if { share_cookbook_to_supermarket? }
34 | action :install
35 | notifies :write, 'log[notify_user_about_supermarket_gem]'
36 | end
37 |
38 | log 'notify_user_about_supermarket_gem' do
39 | message "\nGEM DEPRECATED: The `knife-supermarket` gem has been deprecated " \
40 | 'and the `knife supermarket` subcommands have been moved in to core ' \
41 | 'Chef. Please ensure you have ChefDK 0.14 or newer on your build nodes.'
42 | level :warn
43 | action :nothing
44 | end
45 |
--------------------------------------------------------------------------------
/spec/unit/recipes/provision_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "delivery-truck::provision" do
4 | let(:chef_run) do
5 | @node = nil
6 | ChefSpec::SoloRunner.new do |node|
7 | @node = node
8 | node.default['delivery']['workspace']['root'] = '/tmp'
9 | node.default['delivery']['workspace']['repo'] = '/tmp/repo'
10 | node.default['delivery']['workspace']['chef'] = '/tmp/chef'
11 | node.default['delivery']['workspace']['cache'] = '/tmp/cache'
12 |
13 | node.default['delivery']['change']['enterprise'] = 'Chef'
14 | node.default['delivery']['change']['organization'] = 'Delivery'
15 | node.default['delivery']['change']['project'] = 'Secret'
16 | node.default['delivery']['change']['pipeline'] = 'master'
17 | node.default['delivery']['change']['change_id'] = 'aaaa-bbbb-cccc'
18 | node.default['delivery']['change']['patchset_number'] = '1'
19 | node.default['delivery']['change']['stage'] = 'union'
20 | node.default['delivery']['change']['phase'] = 'provision'
21 | node.default['delivery']['change']['git_url'] = 'https://git.co/my_project.git'
22 | node.default['delivery']['change']['sha'] = '0123456789abcdef'
23 | node.default['delivery']['change']['patchset_branch'] = 'mypatchset/branch'
24 | end.converge(described_recipe)
25 | end
26 |
27 | before do
28 | allow(Chef::Config).to receive(:from_file).with('/var/opt/delivery/workspace/.chef/knife.rb').and_return(true)
29 | end
30 |
31 | it 'copy env from prior to current' do
32 | expect(chef_run).to run_ruby_block('copy env from prior to current')
33 | expect(::DeliveryTruck::Helpers::Provision).to receive(:provision).with('union', @node, 'acceptance-Chef-Delivery-Secret-master', [])
34 |
35 | chef_run.find_resources(:ruby_block).first.old_run_action(:create)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/chefignore:
--------------------------------------------------------------------------------
1 | # Put files/directories that should be ignored in this file when uploading
2 | # to a Chef Infra Server or Supermarket.
3 | # Lines that start with '# ' are comments.
4 |
5 | # OS generated files #
6 | ######################
7 | .DS_Store
8 | ehthumbs.db
9 | Icon?
10 | nohup.out
11 | Thumbs.db
12 | .envrc
13 |
14 | # EDITORS #
15 | ###########
16 | .#*
17 | .project
18 | .settings
19 | *_flymake
20 | *_flymake.*
21 | *.bak
22 | *.sw[a-z]
23 | *.tmproj
24 | *~
25 | \#*
26 | REVISION
27 | TAGS*
28 | tmtags
29 | .vscode
30 | .editorconfig
31 |
32 | ## COMPILED ##
33 | ##############
34 | *.class
35 | *.com
36 | *.dll
37 | *.exe
38 | *.o
39 | *.pyc
40 | *.so
41 | */rdoc/
42 | a.out
43 | mkmf.log
44 |
45 | # Testing #
46 | ###########
47 | .circleci/*
48 | .codeclimate.yml
49 | .delivery/*
50 | .foodcritic
51 | .kitchen*
52 | .mdlrc
53 | .overcommit.yml
54 | .rspec
55 | .rubocop.yml
56 | .travis.yml
57 | .watchr
58 | .yamllint
59 | azure-pipelines.yml
60 | Dangerfile
61 | examples/*
62 | features/*
63 | Guardfile
64 | kitchen.yml*
65 | mlc_config.json
66 | Procfile
67 | Rakefile
68 | spec/*
69 | test/*
70 |
71 | # SCM #
72 | #######
73 | .git
74 | .gitattributes
75 | .gitconfig
76 | .github/*
77 | .gitignore
78 | .gitkeep
79 | .gitmodules
80 | .svn
81 | */.bzr/*
82 | */.git
83 | */.hg/*
84 | */.svn/*
85 |
86 | # Berkshelf #
87 | #############
88 | Berksfile
89 | Berksfile.lock
90 | cookbooks/*
91 | tmp
92 |
93 | # Bundler #
94 | ###########
95 | vendor/*
96 | Gemfile
97 | Gemfile.lock
98 |
99 | # Policyfile #
100 | ##############
101 | Policyfile.rb
102 | Policyfile.lock.json
103 |
104 | # Documentation #
105 | #############
106 | CODE_OF_CONDUCT*
107 | CONTRIBUTING*
108 | documentation/*
109 | TESTING*
110 | UPGRADING*
111 |
112 | # Vagrant #
113 | ###########
114 | .vagrant
115 | Vagrantfile
116 |
--------------------------------------------------------------------------------
/libraries/helpers_deploy.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | module DeliveryTruck
19 | module Helpers
20 | module Deploy
21 | extend self
22 |
23 | # Read the Delivery Config to see if the user has indicated an
24 | # specific deployment search query to use
25 | #
26 | # @param [Chef::Node] Chef Node object
27 | # @return [String] The deployment search query
28 | def deployment_search_query(node)
29 | node['delivery']['config']['delivery-truck']['deploy']['search']
30 | rescue
31 | 'recipes:*push-jobs*'
32 | end
33 |
34 | def delivery_chef_server_search(type, query, delivery_knife_rb)
35 | results = []
36 | DeliverySugar::ChefServer.new(delivery_knife_rb).with_server_config do
37 | ::Chef::Search::Query.new.search(type, query) { |o| results << o }
38 | end
39 | results
40 | end
41 | end
42 | end
43 |
44 | module DSL
45 | def delivery_chef_server_search(type, query)
46 | DeliveryTruck::Helpers::Deploy.delivery_chef_server_search(type, query, delivery_knife_rb)
47 | end
48 |
49 | # Check config.json to get deployment search query
50 | def deployment_search_query
51 | DeliveryTruck::Helpers::Deploy.deployment_search_query(node)
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/recipes/lint.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | changed_cookbooks.each do |cookbook|
19 | # Run Foodcritic against any cookbooks that were modified.
20 | execute "lint_foodcritic_#{cookbook.name}" do
21 | command "foodcritic #{foodcritic_fail_tags} #{foodcritic_tags} " \
22 | "#{foodcritic_excludes} #{cookbook.path}"
23 | end
24 | # If cookstyle is enabled in config.json, run cookstyle against any
25 | # cookbooks that were modified. Otherwise, run rubocop against any
26 | # modified cookbooks, if the cookbook contains a .rubocop.yml file
27 | if cookstyle_enabled?
28 | execute "lint_cookstyle_#{cookbook.name}" do
29 | command "cookstyle #{cookbook.path}"
30 | environment(
31 | # workaround for https://github.com/bbatsov/rubocop/issues/2407
32 | 'USER' => (ENV['USER'] || 'dbuild')
33 | )
34 | live_stream true
35 | only_if 'cookstyle -v'
36 | end
37 | else
38 | execute "lint_rubocop_#{cookbook.name}" do
39 | command "rubocop #{cookbook.path}"
40 | environment(
41 | # workaround for https://github.com/bbatsov/rubocop/issues/2407
42 | 'USER' => (ENV['USER'] || 'dbuild')
43 | )
44 | live_stream true
45 | only_if { ::File.exist?(File.join(cookbook.path, '.rubocop.yml')) }
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/.delivery/build_cookbook/recipes/deploy.rb:
--------------------------------------------------------------------------------
1 | include_recipe 'delivery-truck::default'
2 |
3 | # Modifying the Release Process
4 | #
5 | # Stage 1
6 | # We will continue pushing to Github until all our customers point
7 | # their build-cookbooks to pull from Supermarket instead. In Stage 2
8 | # we will move the push to Github process to Acceptance.
9 | if delivery_environment == 'delivered' # <- Delete on Stage 2
10 |
11 | # In Acceptance
12 | #
13 | # We want to push changes to Github so we can test other cookbooks
14 | # that use delivery-truck. This will allow us to know if it is working
15 | # fine or not. Then when we Deliver we will share it to Supermarket
16 | #if delivery_environment == get_acceptance_environment # <- Uncomment on Stage 2
17 | # Pull the encrypted secrets from the Chef Server
18 | secrets = get_project_secrets
19 |
20 | github_repo = node['delivery']['config']['delivery-truck']['publish']['github']
21 |
22 | delivery_github github_repo do
23 | deploy_key secrets['github']
24 | branch node['delivery']['change']['pipeline']
25 | remote_url "git@github.com:#{github_repo}.git"
26 | repo_path node['delivery']['workspace']['repo']
27 | cache_path node['delivery']['workspace']['cache']
28 | action :push
29 | end
30 | #end # <- Uncomment on Stage 2
31 |
32 | # In Delivered
33 | #
34 | # We want to share the build-cookbook to supermarket release it officially.
35 | #if delivery_environment == 'delivered' # <- Uncomment on Stage 2
36 |
37 | if use_custom_supermarket_credentials?
38 | #secrets = get_project_secrets # <- Uncomment on Stage 2
39 | if secrets['supermarket_user'].nil? || secrets['supermarket_user'].empty?
40 | raise RuntimeError, 'If supermarket-custom-credentials is set to true, ' \
41 | 'you must add supermarket_user to the secrets data bag.' \
42 | end
43 |
44 | if secrets['supermarket_key'].nil? || secrets['supermarket_key'].nil?
45 | raise RuntimeError, 'If supermarket-custom-credentials is set to true, ' \
46 | 'you must add supermarket_key to the secrets data bag.'
47 | end
48 | end
49 |
50 | delivery_supermarket 'share_delivery_truck_to_supermarket' do
51 | site node['delivery']['config']['delivery-truck']['publish']['supermarket']
52 | if use_custom_supermarket_credentials?
53 | user secrets['supermarket_user']
54 | key secrets['supermarket_key']
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/libraries/delivery_api_client.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2016 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | require 'net/http'
19 |
20 | module DeliveryTruck
21 | module DeliveryApiClient
22 | class BadApiResponse < StandardError
23 | end
24 |
25 | # Determines the list of bocked projects
26 | # @params Node object to pull the enterprise from.
27 | # @returns An array of blocked projects. If the api doesn't exist returns [].
28 | def self.blocked_projects(node)
29 | # Ask the API about how things are looking in union
30 | ent_name = node['delivery']['change']['enterprise']
31 | request_url = "/api/v0/e/#{ent_name}/blocked_projects"
32 | change = get_change_hash(node)
33 | uri = URI.parse(change['delivery_api_url'])
34 | http_client = Net::HTTP.new(uri.host, uri.port)
35 |
36 | if uri.scheme == "https"
37 | http_client.use_ssl = true
38 | http_client.verify_mode = OpenSSL::SSL::VERIFY_NONE
39 | end
40 | result = http_client.get(request_url, get_headers(change['token']))
41 |
42 | case
43 | when result.code == "404"
44 | Chef::Log.info("HTTP 404 recieved from #{request_url}. Please upgrade your Delivery Server.")
45 | []
46 | when result.code.match(/20\d/)
47 | JSON.parse(result.body)['blocked_projects']
48 | else # not success or 404
49 | error_str = "Failed request to #{request_url} returned #{result.code}"
50 | Chef::Log.fatal(error_str)
51 | raise BadApiResponse.new(error_str)
52 | end
53 | end
54 |
55 | def self.get_headers(token)
56 | {"chef-delivery-token" => token,
57 | "chef-delivery-user" => 'builder'}
58 | end
59 |
60 | def self.get_change_hash(node)
61 | change_file = ::File.read(::File.expand_path('../../../../../../../change.json', node['delivery_builder']['workspace']))
62 | change_hash = ::JSON.parse(change_file)
63 | end
64 |
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/recipes/syntax.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | changed_cookbooks.each do |cookbook|
19 | # If we changed a cookbook but didn't bump the version than the build
20 | # phase will fail when trying to upload to the Chef Server.
21 | unless bumped_version?(cookbook.path)
22 | raise RuntimeError,
23 | %{The #{cookbook.name} cookbook was modified but the version was not updated in the metadata file.
24 |
25 | The version must be updated when any of the following files are modified:
26 | metadata.(rb|json)
27 | Berksfile
28 | Berksfile.lock
29 | Policyfile.rb
30 | Policyfile.lock.json
31 | attributes/.*
32 | definitions/.*
33 | files/.*
34 | libraries/.*
35 | providers/.*
36 | recipes/.*
37 | resources/.*
38 | templates/.*}
39 | end
40 |
41 | # Run `knife cookbook test` against the modified cookbook
42 | execute "syntax_check_#{cookbook.name}" do
43 | command "knife cookbook test -o #{cookbook.path} -a"
44 | end
45 | end
46 |
47 | # Temporal Test - Modifying the Release process Stage 1
48 | #
49 | # This provisional test will verify if there are berks dependencies
50 | # pointing to Github, if that is the case we will log a WARN message
51 | # notifying the user that they need to modify their Berksfile
52 | #
53 | # TODO: Remove this on Stage 2
54 | berksfile = ::File.join(delivery_workspace_chef, 'build_cookbook', 'Berksfile')
55 | if ::File.exist?(berksfile)
56 | content = ::File.read(berksfile)
57 | %W(delivery-sugar delivery-truck).each do |build_cookbook|
58 | if content.include?("chef-cookbooks/#{build_cookbook}")
59 | log "warning_#{build_cookbook}_pull_from_github" do
60 | message "Your build-cookbook depends on #{build_cookbook} that is being pulled " \
61 | 'from Github, please modify your Berksfile so that you consume it from ' \
62 | 'Supermarket instead.'
63 | level :warn
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/spec/unit/libraries/helpers_publish_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DeliveryTruck::Helpers::Publish do
4 | let(:node) { Chef::Node.new }
5 |
6 | shared_examples_for 'DSL methods that hard convert node values to a boolean' do
7 | context 'when config value is unset' do
8 | it 'returns false' do
9 | expect(described_class.send(method, node)).to eql(false)
10 | end
11 | end
12 |
13 | context 'when config value is set' do
14 | it 'returns the value' do
15 | node.default['delivery']['config']['delivery-truck']['publish'][node_field] = true_node_attribute
16 | expect(described_class.send(method, node)).to eql(true)
17 |
18 | node.default['delivery']['config']['delivery-truck']['publish'][node_field] = false_node_attribute
19 | expect(described_class.send(method, node)).to eql(false)
20 | end
21 | end
22 | end
23 |
24 | describe '.upload_cookbook_to_chef_server?' do
25 | let(:method) { :upload_cookbook_to_chef_server? }
26 | let(:node_field) { 'chef_server' }
27 | let(:true_node_attribute) { true }
28 | let(:false_node_attribute) { false }
29 |
30 | it_behaves_like 'DSL methods that hard convert node values to a boolean'
31 | end
32 |
33 | describe '.share_cookbook_to_supermarket?' do
34 | let(:method) { :share_cookbook_to_supermarket? }
35 | let(:node_field) { 'supermarket' }
36 | let(:true_node_attribute) { 'https://supermarket.chef.io' }
37 | let(:false_node_attribute) { false }
38 |
39 | it_behaves_like 'DSL methods that hard convert node values to a boolean'
40 | end
41 |
42 | describe '.use_custom_supermarket_credentials?' do
43 | let(:method) { :use_custom_supermarket_credentials? }
44 | let(:node_field) { 'supermarket-custom-credentials' }
45 | let(:true_node_attribute) { true }
46 | let(:false_node_attribute) { false }
47 |
48 | it_behaves_like 'DSL methods that hard convert node values to a boolean'
49 | end
50 |
51 | describe '.push_repo_to_github?' do
52 | let(:method) { :push_repo_to_github? }
53 | let(:node_field) { 'github' }
54 | let(:true_node_attribute) { true }
55 | let(:false_node_attribute) { false }
56 |
57 | it_behaves_like 'DSL methods that hard convert node values to a boolean'
58 | end
59 |
60 | describe '.push_repo_to_git?' do
61 | let(:method) { :push_repo_to_git? }
62 | let(:node_field) { 'git' }
63 | let(:true_node_attribute) { true }
64 | let(:false_node_attribute) { false }
65 |
66 | it_behaves_like 'DSL methods that hard convert node values to a boolean'
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/spec/unit/recipes/unit_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "delivery-truck::unit" do
4 | let(:chef_run) do
5 | ChefSpec::SoloRunner.new do |node|
6 | node.default['delivery']['workspace']['root'] = '/tmp'
7 | node.default['delivery']['workspace']['repo'] = '/tmp/repo'
8 | node.default['delivery']['workspace']['chef'] = '/tmp/chef'
9 | node.default['delivery']['workspace']['cache'] = '/tmp/cache'
10 |
11 | node.default['delivery']['change']['enterprise'] = 'Chef'
12 | node.default['delivery']['change']['organization'] = 'Delivery'
13 | node.default['delivery']['change']['project'] = 'Secret'
14 | node.default['delivery']['change']['pipeline'] = 'master'
15 | node.default['delivery']['change']['change_id'] = 'aaaa-bbbb-cccc'
16 | node.default['delivery']['change']['patchset_number'] = '1'
17 | node.default['delivery']['change']['stage'] = 'union'
18 | node.default['delivery']['change']['phase'] = 'deploy'
19 | node.default['delivery']['change']['git_url'] = 'https://git.co/my_project.git'
20 | node.default['delivery']['change']['sha'] = '0123456789abcdef'
21 | node.default['delivery']['change']['patchset_branch'] = 'mypatchset/branch'
22 | end.converge(described_recipe)
23 | end
24 |
25 | context "when a single cookbook has been modified" do
26 | before do
27 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(one_changed_cookbook)
28 | allow(DeliveryTruck::Helpers::Unit).to receive(:has_spec_tests?).with('/tmp/repo/cookbooks/julia').and_return(true)
29 | end
30 |
31 | it "runs test-kitchen against only that cookbook" do
32 | expect(chef_run).to run_execute("unit_rspec_julia").with(
33 | :cwd => "/tmp/repo/cookbooks/julia",
34 | :command => "rspec --format documentation --color"
35 | )
36 | expect(chef_run).not_to run_execute("unit_rspec_gordon")
37 | expect(chef_run).not_to run_execute("unit_rspec_emeril")
38 | end
39 | end
40 |
41 | context "when multiple cookbooks have been modified" do
42 | before do
43 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(two_changed_cookbooks)
44 | allow(DeliveryTruck::Helpers::Unit).to receive(:has_spec_tests?).with('/tmp/repo/cookbooks/julia').and_return(true)
45 | allow(DeliveryTruck::Helpers::Unit).to receive(:has_spec_tests?).with('/tmp/repo/cookbooks/gordon').and_return(true)
46 | end
47 |
48 | it "runs test-kitchen against only those cookbooks" do
49 | expect(chef_run).to run_execute("unit_rspec_julia").with(
50 | :cwd => "/tmp/repo/cookbooks/julia",
51 | :command => "rspec --format documentation --color"
52 | )
53 | expect(chef_run).to run_execute("unit_rspec_gordon").with(
54 | :cwd => "/tmp/repo/cookbooks/gordon",
55 | :command => "rspec --format documentation --color"
56 | )
57 | expect(chef_run).not_to run_execute("unit_rspec_emeril")
58 | end
59 | end
60 |
61 | context "when no cookbooks have been modified" do
62 | before do
63 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(no_changed_cookbooks)
64 | end
65 |
66 | it "does not run test-kitchen against any cookbooks" do
67 | expect(chef_run).not_to run_execute("unit_rspec_julia")
68 | expect(chef_run).not_to run_execute("unit_rspec_gordon")
69 | expect(chef_run).not_to run_execute("unit_rspec_emeril")
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/libraries/helpers_syntax.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | require 'pathname'
19 |
20 | module DeliveryTruck
21 | module Helpers
22 | module Syntax
23 | extend self
24 |
25 | # Check whether or not the metadata file was modified when
26 | # cookbook-related files were modified.
27 | #
28 | # Note: The concept of "the version of the cookbook at the merge base'
29 | # is inherently flawed. You can rename a cookbook in the metadata.rb and
30 | # leave it in the same path. You can move a cookbook to a new path and
31 | # could have edited it after the move. The cookbook might not exist in
32 | # this repo at the merge base - imagine migrating cookbooks from one repo
33 | # to another. It would next to impossible for delivery to correctly guess
34 | # the correct "base" version of this cookbook. We simply assume that if
35 | # a base cookbook were to exist, it exists at the same location with the
36 | # same name.
37 | #
38 | # @param path [String] The path to the cookbook
39 | # @param node [Chef::Node]
40 | #
41 | # @return [TrueClass, FalseClass]
42 | #
43 | def bumped_version?(path, node)
44 | change = DeliverySugar::Change.new(node)
45 | modified_files = change.changed_files
46 |
47 | cookbook_path = Pathname.new(path)
48 | workspace_repo = Pathname.new(change.workspace_repo)
49 | relative_dir = cookbook_path.relative_path_from(workspace_repo).to_s
50 | files_to_check = %W(
51 | metadata\.(rb|json)
52 | Berksfile
53 | Berksfile\.lock
54 | Policyfile\.rb
55 | Policyfile\.lock\.json
56 | attributes\/.*
57 | definitions\/.*
58 | files\/.*
59 | libraries\/.*
60 | providers\/.*
61 | recipes\/.*
62 | resources\/.*
63 | templates\/.*
64 | ).join('|')
65 |
66 | clean_relative_dir = relative_dir == "." ? "" : Regexp.escape("#{relative_dir}/")
67 |
68 | if modified_files.any? { |f| /^#{clean_relative_dir}(#{files_to_check})/ =~ f }
69 | base = change.merge_sha.empty? ? "origin/#{change.pipeline}" : "#{change.merge_sha}~1"
70 | base_metadata = change.cookbook_metadata(path, base)
71 | base_metadata.nil? ||
72 | change.cookbook_metadata(path).version != base_metadata.version
73 | else
74 | # We return true here as an indication that we should not fail checks.
75 | # In reality we simply did not change any files that would require us
76 | # to bump our version number in our metadata.rb.
77 | true
78 | end
79 | end
80 | end
81 | end
82 |
83 | module DSL
84 | def bumped_version?(path)
85 | DeliveryTruck::Helpers::Syntax.bumped_version?(path, node)
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'chefspec'
2 |
3 | TOPDIR = File.expand_path(File.join(File.dirname(__FILE__), ".."))
4 | $: << File.expand_path(File.dirname(__FILE__))
5 |
6 | # Require all our libraries
7 | Dir['libraries/*.rb'].each { |f| require File.expand_path(f) }
8 |
9 | # Alright this is going to get crazy! :)
10 | #
11 | # PROBLEM: We would like to eat our own dogfood at the earliest Stage
12 | # in Delivery, that means we need to pull delivery-sugar from DCC.
13 | # The problem is that we can't release delivery-truck with this
14 | # dependency because end-users won't be able to reach it
15 | #
16 | # For this reason we are going to inject the dependency before we
17 | # run `berks install` inside chefspec. With that we will run our
18 | # tests using the latest delivery-sugar cookbook and without issues
19 | # in the release process
20 | def delivery_sugar_dcc_dependency
21 | < 'julia',
78 | :path => '/tmp/repo/cookbooks/julia',
79 | :version => '0.1.0'
80 | )
81 | ]}
82 |
83 | let(:two_changed_cookbooks) {[
84 | double(
85 | 'delivery sugar cookbook',
86 | :name => 'julia',
87 | :path => '/tmp/repo/cookbooks/julia',
88 | :version => '0.1.0'
89 | ),
90 | double(
91 | 'delivery sugar cookbook',
92 | :name => 'gordon',
93 | :path => '/tmp/repo/cookbooks/gordon',
94 | :version => '0.2.0'
95 | )
96 | ]}
97 |
98 | let(:no_changed_cookbooks) {[]}
99 | end
100 |
101 | RSpec.configure do |config|
102 | config.include SharedLetDeclarations
103 | config.filter_run_excluding :ignore => true
104 | config.filter_run focus: true
105 | config.run_all_when_everything_filtered = true
106 |
107 | # Specify the operating platform to mock Ohai data from (default: nil)
108 | config.platform = 'ubuntu'
109 |
110 | # Specify the operating version to mock Ohai data from (default: nil)
111 | config.version = '12.04'
112 | end
113 |
--------------------------------------------------------------------------------
/libraries/helpers_publish.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | module DeliveryTruck
19 | module Helpers
20 | module Publish
21 | extend self
22 |
23 | # Read the Delivery Config to see if the user has indicated they want their
24 | # cookbooks uploaded to Delivery's Chef Server. This is an intermediate
25 | # solution until more flexible endpoints are developed.
26 | #
27 | # @param [Chef::Node] Chef Node object
28 | # @return [TrueClass, FalseClass]
29 | def upload_cookbook_to_chef_server?(node)
30 | node['delivery']['config']['delivery-truck']['publish']['chef_server']
31 | rescue
32 | false
33 | end
34 |
35 | # Read the Delivery Config to see if the user has indicated a Github
36 | # repo they would like to push to.
37 | #
38 | # @param [Chef::Node] Chef Node object
39 | # @return [TrueClass, FalseClass]
40 | def push_repo_to_github?(node)
41 | !!node['delivery']['config']['delivery-truck']['publish']['github']
42 | rescue
43 | false
44 | end
45 |
46 | # Read the Delivery Config to see if the user has indicated a Git Server
47 | # repo they would like to push to.
48 | #
49 | # @param [Chef::Node] Chef Node object
50 | # @return [TrueClass, FalseClass]
51 | def push_repo_to_git?(node)
52 | !!node['delivery']['config']['delivery-truck']['publish']['git']
53 | rescue
54 | false
55 | end
56 |
57 | # Read the Delivery Config to see if the user has indicated a Supermarket
58 | # Server they would like to share to.
59 | #
60 | # @param [Chef::Node] Chef Node object
61 | # @return [TrueClass, FalseClass]
62 | def share_cookbook_to_supermarket?(node)
63 | !!node['delivery']['config']['delivery-truck']['publish']['supermarket']
64 | rescue
65 | false
66 | end
67 |
68 | # Read the Delivery Config to see if the user has indicated custom credentials
69 | # should be used instead of those found in delivery_knife_rb. If so, they should
70 | # loaded from via get_project_secrets.
71 | #
72 | # @param [Chef::Node] Chef Node object
73 | # @return [TrueClass, FalseClass]
74 | def use_custom_supermarket_credentials?(node)
75 | !!node['delivery']['config']['delivery-truck']['publish']['supermarket-custom-credentials']
76 | rescue
77 | false
78 | end
79 | end
80 | end
81 |
82 | module DSL
83 | # Check config.json for whether user wants to upload to Chef Server
84 | def upload_cookbook_to_chef_server?
85 | DeliveryTruck::Helpers::Publish.upload_cookbook_to_chef_server?(node)
86 | end
87 |
88 | # Check config.json for whether user wants to push to Github
89 | def push_repo_to_github?
90 | DeliveryTruck::Helpers::Publish.push_repo_to_github?(node)
91 | end
92 |
93 | # Check config.json for whether user wants to push to a Git Server
94 | def push_repo_to_git?
95 | DeliveryTruck::Helpers::Publish.push_repo_to_git?(node)
96 | end
97 |
98 | # Check config.json for whether user wants to share to Supermarket
99 | def share_cookbook_to_supermarket?
100 | DeliveryTruck::Helpers::Publish.share_cookbook_to_supermarket?(node)
101 | end
102 |
103 | # Check config.json for whether user wants to share to Supermarket
104 | def use_custom_supermarket_credentials?
105 | DeliveryTruck::Helpers::Publish.use_custom_supermarket_credentials?(node)
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/spec/unit/recipes/lint_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "delivery-truck::lint" do
4 | let(:chef_run) do
5 | ChefSpec::SoloRunner.new do |node|
6 | node.default['delivery']['workspace']['root'] = '/tmp'
7 | node.default['delivery']['workspace']['repo'] = '/tmp/repo'
8 | node.default['delivery']['workspace']['chef'] = '/tmp/chef'
9 | node.default['delivery']['workspace']['cache'] = '/tmp/cache'
10 |
11 | node.default['delivery']['change']['enterprise'] = 'Chef'
12 | node.default['delivery']['change']['organization'] = 'Delivery'
13 | node.default['delivery']['change']['project'] = 'Secret'
14 | node.default['delivery']['change']['pipeline'] = 'master'
15 | node.default['delivery']['change']['change_id'] = 'aaaa-bbbb-cccc'
16 | node.default['delivery']['change']['patchset_number'] = '1'
17 | node.default['delivery']['change']['stage'] = 'union'
18 | node.default['delivery']['change']['phase'] = 'deploy'
19 | node.default['delivery']['change']['git_url'] = 'https://git.co/my_project.git'
20 | node.default['delivery']['change']['sha'] = '0123456789abcdef'
21 | node.default['delivery']['change']['patchset_branch'] = 'mypatchset/branch'
22 | end.converge(described_recipe)
23 | end
24 |
25 | context "when a single cookbook has been modified" do
26 | before do
27 | allow(DeliveryTruck::Helpers::Lint).to receive(:foodcritic_epic_fail).and_return("-f correctness")
28 | allow(DeliveryTruck::Helpers::Lint).to receive(:foodcritic_tags).and_return("-t FC001")
29 | allow(DeliveryTruck::Helpers::Lint).to receive(:foodcritic_excludes).and_return("--exclude spec")
30 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(one_changed_cookbook)
31 | end
32 |
33 | it "runs test-kitchen against only that cookbook" do
34 | expect(chef_run).to run_execute("lint_foodcritic_julia").with(
35 | :command => "foodcritic -f correctness -t FC001 --exclude spec /tmp/repo/cookbooks/julia"
36 | )
37 | expect(chef_run).not_to run_execute("lint_foodcritic_gordon")
38 | expect(chef_run).not_to run_execute("lint_foodcritic_emeril")
39 | end
40 | end
41 |
42 | context "when multiple cookbooks have been modified" do
43 | before do
44 | allow(DeliveryTruck::Helpers::Lint).to receive(:foodcritic_epic_fail).and_return("-f correctness")
45 | allow(DeliveryTruck::Helpers::Lint).to receive(:foodcritic_tags).and_return("-t ~FC002")
46 | allow(DeliveryTruck::Helpers::Lint).to receive(:foodcritic_excludes).and_return("--exclude test")
47 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(two_changed_cookbooks)
48 | end
49 |
50 | it "runs test-kitchen against only those cookbooks" do
51 | expect(chef_run).to run_execute("lint_foodcritic_julia").with(
52 | :command => "foodcritic -f correctness -t ~FC002 --exclude test /tmp/repo/cookbooks/julia"
53 | )
54 | expect(chef_run).to run_execute("lint_foodcritic_gordon").with(
55 | :command => "foodcritic -f correctness -t ~FC002 --exclude test /tmp/repo/cookbooks/gordon"
56 | )
57 | expect(chef_run).not_to run_execute("lint_foodcritic_emeril")
58 | end
59 | end
60 |
61 | context "when no cookbooks have been modified" do
62 | before do
63 | allow(DeliveryTruck::Helpers::Lint).to receive(:foodcritic_tags).and_return("")
64 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(no_changed_cookbooks)
65 | end
66 |
67 | it "does not run test-kitchen against any cookbooks" do
68 | expect(chef_run).not_to run_execute("lint_foodcritic_julia")
69 | expect(chef_run).not_to run_execute("lint_foodcritic_gordon")
70 | expect(chef_run).not_to run_execute("lint_foodcritic_emeril")
71 | end
72 | end
73 |
74 | context "when a .rubocop.yml is present" do
75 | before do
76 | allow(DeliveryTruck::Helpers::Lint).to receive(:foodcritic_epic_fail).and_return("-f correctness")
77 | allow(DeliveryTruck::Helpers::Lint).to receive(:foodcritic_tags).and_return("")
78 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(one_changed_cookbook)
79 | allow(File).to receive(:exist?).and_call_original
80 | allow(File).to receive(:exist?).with("/tmp/repo/cookbooks/julia/.rubocop.yml").and_return(true)
81 | end
82 |
83 | it "runs Rubocop" do
84 | expect(chef_run).to run_execute("lint_rubocop_julia").with(
85 | :command => "rubocop /tmp/repo/cookbooks/julia"
86 | )
87 | expect(chef_run).not_to run_execute("lint_rubocop_gordon")
88 | expect(chef_run).not_to run_execute("lint_rubocop_emeril")
89 | end
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/recipes/publish.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | # The intended purpose of this recipe is to publish modified files to the
19 | # necessary endpoints like Chef Servers or Supermarkets. The specific details
20 | # about what to publish and where to publish it will be specified in the
21 | # `.delivery/config.json` file. Please see the
22 | # [`delivery-truck` cookbook's README](README.md) for
23 | # additional configuration details.
24 |
25 | # If the user specified a supermarket server to share to, share it
26 | if share_cookbook_to_supermarket?
27 | # Load secrets if custom_supermarket_credentials was specified
28 | if use_custom_supermarket_credentials?
29 | secrets = get_project_secrets
30 | if secrets['supermarket_user'].nil? || secrets['supermarket_user'].empty?
31 | raise RuntimeError, 'If supermarket-custom-credentials is set to true, ' \
32 | 'you must add supermarket_user to the secrets data bag.'
33 | end
34 |
35 | if secrets['supermarket_key'].nil? || secrets['supermarket_key'].nil?
36 | raise RuntimeError, 'If supermarket-custom-credentials is set to true, ' \
37 | 'you must add supermarket_key to the secrets data bag.'
38 | end
39 | end
40 |
41 | changed_cookbooks.each do |cookbook|
42 | delivery_supermarket "share_#{cookbook.name}_to_supermarket" do
43 | site node['delivery']['config']['delivery-truck']['publish']['supermarket']
44 | cookbook cookbook.name
45 | version cookbook.version
46 | path cookbook.path
47 | if use_custom_supermarket_credentials?
48 | user secrets['supermarket_user']
49 | key secrets['supermarket_key']
50 | end
51 | end
52 | end
53 | end
54 |
55 | # Create the upload directory where cookbooks to be uploaded will be staged
56 | cookbook_directory = File.join(node['delivery']['workspace']['cache'], 'cookbook-upload')
57 | directory cookbook_directory do
58 | recursive true
59 | # We delete the cookbook upload staging directory each time to ensure we
60 | # don't have out-of-date cookbooks hanging around from a previous build.
61 | action [:delete, :create]
62 | end
63 |
64 | # Upload each cookbook to the Chef Server
65 | if upload_cookbook_to_chef_server?
66 | changed_cookbooks.each do |cookbook|
67 | if File.exist?(File.join(cookbook.path, 'Berksfile'))
68 | execute "berks_vendor_cookbook_#{cookbook.name}" do
69 | command "berks vendor #{cookbook_directory}"
70 | cwd cookbook.path
71 | end
72 | else
73 | link ::File.join(cookbook_directory, cookbook.name) do
74 | to cookbook.path
75 | end
76 | end
77 |
78 | execute "upload_cookbook_#{cookbook.name}" do
79 | command "knife cookbook upload #{cookbook.name} --freeze --all --force " \
80 | "--config #{delivery_knife_rb} " \
81 | "--cookbook-path #{cookbook_directory}"
82 | end
83 | end
84 | end
85 |
86 | # If the user specified a github repo to push to, push to that repo
87 | if push_repo_to_github?
88 | secrets = get_project_secrets
89 | github_repo = node['delivery']['config']['delivery-truck']['publish']['github']
90 |
91 | delivery_github github_repo do
92 | deploy_key secrets['github']
93 | branch node['delivery']['change']['pipeline']
94 | remote_url "git@github.com:#{github_repo}.git"
95 | repo_path node['delivery']['workspace']['repo']
96 | cache_path node['delivery']['workspace']['cache']
97 | action :push
98 | end
99 | end
100 |
101 | # If the user specified a general git repo to push to, push to that repo
102 | if push_repo_to_git?
103 | secrets = get_project_secrets
104 | git_repo = node['delivery']['config']['delivery-truck']['publish']['git']
105 |
106 | delivery_github git_repo do
107 | deploy_key secrets['git']
108 | branch node['delivery']['change']['pipeline']
109 | remote_url git_repo
110 | repo_path node['delivery']['workspace']['repo']
111 | cache_path node['delivery']['workspace']['cache']
112 | action :push
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/spec/unit/recipes/deploy_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | # Simple FakeNode to muck Chef::Node class
4 | class MyFakeNode
5 | attr_reader :name
6 |
7 | def initialize(name)
8 | @name = name
9 | end
10 | end
11 |
12 | describe "delivery-truck::deploy" do
13 | let(:chef_run) do
14 | ChefSpec::SoloRunner.new do |node|
15 | node.default['delivery']['workspace']['root'] = '/tmp'
16 | node.default['delivery']['workspace']['repo'] = '/tmp/repo'
17 | node.default['delivery']['workspace']['chef'] = '/tmp/chef'
18 | node.default['delivery']['workspace']['cache'] = '/tmp/cache'
19 |
20 | node.default['delivery']['change']['enterprise'] = 'Chef'
21 | node.default['delivery']['change']['organization'] = 'Delivery'
22 | node.default['delivery']['change']['project'] = 'Secret'
23 | node.default['delivery']['change']['pipeline'] = 'master'
24 | node.default['delivery']['change']['change_id'] = 'aaaa-bbbb-cccc'
25 | node.default['delivery']['change']['patchset_number'] = '1'
26 | node.default['delivery']['change']['stage'] = 'union'
27 | node.default['delivery']['change']['phase'] = 'deploy'
28 | node.default['delivery']['change']['git_url'] = 'https://git.co/my_project.git'
29 | node.default['delivery']['change']['sha'] = '0123456789abcdef'
30 | node.default['delivery']['change']['patchset_branch'] = 'mypatchset/branch'
31 | end.converge(described_recipe)
32 | end
33 |
34 | let(:search_query) do
35 | "(#{recipe_list}) AND chef_environment:union AND recipes:*push-jobs*"
36 | end
37 | let(:node_list) { [MyFakeNode.new("node1"), MyFakeNode.new("node2")] }
38 | let(:delivery_knife_rb) do
39 | "/var/opt/delivery/workspace/.chef/knife.rb"
40 | end
41 |
42 | context "when a single cookbook has been modified" do
43 | before do
44 | allow_any_instance_of(Chef::Recipe).to receive(:get_all_project_cookbooks).and_return(one_changed_cookbook)
45 | allow_any_instance_of(Chef::Recipe).to receive(:get_cookbook_version).and_return('1.0.0')
46 | end
47 |
48 | let(:recipe_list) { 'recipes:julia*' }
49 |
50 | it "deploy only that cookbook" do
51 | expect(DeliveryTruck::Helpers::Deploy).to receive(:delivery_chef_server_search).with(:node, search_query, delivery_knife_rb).and_return(node_list)
52 | expect(chef_run).to dispatch_delivery_push_job("deploy_Secret").with(
53 | :command => 'chef-client',
54 | :nodes => node_list
55 | )
56 | end
57 |
58 | context "and the user sets a different search query" do
59 | before do
60 | allow(DeliveryTruck::Helpers::Deploy).to receive(:deployment_search_query)
61 | .and_return('recipes:my_cool_push_jobs_cookbook AND more:constraints')
62 | end
63 | let(:search_query) do
64 | "(#{recipe_list}) AND chef_environment:union AND recipes:my_cool_push_jobs_cookbook AND more:constraints"
65 | end
66 | it "deploy only that cookbook with the special search query" do
67 | expect(DeliveryTruck::Helpers::Deploy).to receive(:delivery_chef_server_search)
68 | .with(:node, search_query, delivery_knife_rb)
69 | .and_return(node_list)
70 | expect(chef_run).to dispatch_delivery_push_job("deploy_Secret").with(
71 | :command => 'chef-client',
72 | :nodes => node_list
73 | )
74 | end
75 | end
76 | end
77 |
78 | context "when multiple cookbooks have been modified" do
79 | before do
80 | allow_any_instance_of(Chef::Recipe).to receive(:get_all_project_cookbooks).and_return(two_changed_cookbooks)
81 | allow_any_instance_of(Chef::Recipe).to receive(:get_cookbook_version).and_return('1.0.0')
82 | end
83 |
84 | let(:recipe_list) { 'recipes:julia* OR recipes:gordon*' }
85 |
86 | it "deploy only those cookbooks" do
87 | allow_any_instance_of(Chef::Recipe).to receive(:delivery_chef_server_search).with(:node, search_query).and_return(node_list)
88 | expect(chef_run).to dispatch_delivery_push_job("deploy_Secret").with(
89 | :command => 'chef-client',
90 | :nodes => node_list
91 | )
92 | end
93 | end
94 |
95 | context "when no cookbooks have been modified" do
96 | before do
97 | allow_any_instance_of(Chef::Recipe).to receive(:get_all_project_cookbooks).and_return(no_changed_cookbooks)
98 | allow_any_instance_of(Chef::Recipe).to receive(:get_cookbook_version).and_return('1.0.0')
99 | end
100 |
101 | it "does not deploy any cookbooks" do
102 | expect(chef_run).not_to dispatch_delivery_push_job("deploy_Secret")
103 | end
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/libraries/helpers_lint.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | module DeliveryTruck
19 | module Helpers
20 | module Lint
21 | extend self
22 |
23 | # Based on the properties in the Delivery Config, create the tags string
24 | # that will be passed into the foodcritic command.
25 | #
26 | # @param node [Chef::Node] Chef Node object
27 | # @return [String]
28 | def foodcritic_tags(node)
29 | begin
30 | config = node['delivery']['config']['delivery-truck']['lint']['foodcritic']
31 |
32 | # We ignore these two by default since they search for the presence of
33 | # `issues_url` and `source_url` in the metadata.rb. Those fields will
34 | # only be populated by cookbooks uploading to Supermarket.
35 | default_ignore = "-t ~FC064 -t ~FC065"
36 |
37 | # Do not ignore these rules if the cookbook will be share to Supermarket
38 | if ::DeliveryTruck::Helpers::Publish.share_cookbook_to_supermarket?(node)
39 | default_ignore = ""
40 | end
41 |
42 | case
43 | when config.nil?
44 | default_ignore
45 | when config['only_rules'] && !config['only_rules'].empty?
46 | "-t " + config['only_rules'].join(",")
47 | when config['ignore_rules'].nil?
48 | default_ignore
49 | when config['ignore_rules'].empty?
50 | # They can set ignore_rules to an empty Array to disable these defaults
51 | ""
52 | when config['ignore_rules'] && !config['ignore_rules'].empty?
53 | "-t ~" + config['ignore_rules'].join(" -t ~")
54 | else
55 | ""
56 | end
57 | rescue
58 | ""
59 | end
60 | end
61 |
62 | # Based on the properties in the Delivery Config, create the --excludes
63 | # options that will be passed into the foodcritic command.
64 | #
65 | # @param node [Chef::Node] Chef Node object
66 | # @return [String]
67 | def foodcritic_excludes(node)
68 | begin
69 | config = node['delivery']['config']['delivery-truck']['lint']['foodcritic']
70 | case
71 | when config['excludes'] && !config['excludes'].empty?
72 | "--exclude " + config['excludes'].join(" --exclude ")
73 | else
74 | ""
75 | end
76 | rescue
77 | ""
78 | end
79 | end
80 |
81 | # Based on the properties in the Delivery Config, create the --epic_fail
82 | # (-f) tags that will be passed into the foodcritic command.
83 | #
84 | # @param node [Chef::Node] Chef Node object
85 | # @return [String]
86 | def foodcritic_fail_tags(node)
87 | config = node['delivery']['config']['delivery-truck']['lint']['foodcritic']
88 | case
89 | when config['fail_tags'] && !config['fail_tags'].empty?
90 | '-f ' + config['fail_tags'].join(',')
91 | else
92 | '-f correctness'
93 | end
94 | rescue
95 | '-f correctness'
96 | end
97 | # Read the Delivery Config to see if the user has indicated they want to
98 | # run cookstyle tests on their cookbook
99 | #
100 | # @param [Chef::Node] Chef Node object
101 | # @return [TrueClass, FalseClass]
102 | def cookstyle_enabled?(node)
103 | node['delivery']['config']['delivery-truck']['lint']['enable_cookstyle']
104 | rescue
105 | false
106 | end
107 |
108 | end
109 | end
110 |
111 | module DSL
112 |
113 | # Return the applicable tags for foodcritic runs
114 | def foodcritic_tags
115 | DeliveryTruck::Helpers::Lint.foodcritic_tags(node)
116 | end
117 |
118 | # Return the applicable excludes for foodcritic runs
119 | def foodcritic_excludes
120 | DeliveryTruck::Helpers::Lint.foodcritic_excludes(node)
121 | end
122 |
123 | # Return the fail tags for foodcritic runs
124 | def foodcritic_fail_tags
125 | DeliveryTruck::Helpers::Lint.foodcritic_fail_tags(node)
126 | end
127 |
128 | # Check config.json for whether user wants to share to Supermarket
129 | def cookstyle_enabled?
130 | DeliveryTruck::Helpers::Lint.cookstyle_enabled?(node)
131 | end
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/spec/unit/libraries/delivery_api_client_specs.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DeliveryTruck::DeliveryApiClient do
4 | let(:node) do
5 | {
6 | 'delivery' => { 'change' => {'enterprise' => 'Example_Enterprise'} },
7 | 'delivery_builder' => { 'workspace' => '/var/opt/delivery/workspace' }
8 | }
9 | end
10 |
11 | let(:api_host) { 'delivery.example.com' }
12 | let(:api_url) { 'https://' + api_host }
13 | let(:api_port) { 443 }
14 | let(:api_token) { 'DECAFBAD' }
15 |
16 | let(:change_json) do
17 | JSON.generate({
18 | 'delivery_api_url' => api_url,
19 | 'token' => api_token,
20 | })
21 | end
22 |
23 | let(:expected_headers) do
24 | {
25 | 'chef-delivery-token' => api_token,
26 | 'chef-delivery-user' => 'builder',
27 | }
28 | end
29 |
30 | let(:http_client) { double 'Net::HTTP' }
31 |
32 | before(:each) do
33 | allow(File)
34 | .to receive(:read)
35 | .and_return(change_json)
36 | end
37 |
38 | describe '.blocked_projects' do
39 | let(:blocked_project_api) { '/api/v0/e/Example_Enterprise/blocked_projects' }
40 |
41 | context 'when api url is http' do
42 | let(:api_url) { 'http://' + api_host }
43 | let(:api_port) { 80 }
44 |
45 | it 'does not set ssl settings' do
46 | expect(Net::HTTP).
47 | to receive(:new).
48 | with(api_host, api_port).
49 | and_return(http_client)
50 | expect(http_client).
51 | to receive(:get).
52 | with(blocked_project_api, expected_headers).
53 | and_return(OpenStruct.new({:code => "404"}))
54 | result = DeliveryTruck::DeliveryApiClient.blocked_projects(node)
55 | expect(result).to eql([])
56 | end
57 | end
58 |
59 | context 'when api url is https' do
60 | it 'sets use ssl to true' do
61 | expect(Net::HTTP).
62 | to receive(:new).
63 | with(api_host, api_port).
64 | and_return(http_client)
65 | expect(http_client).
66 | to receive(:use_ssl=).
67 | with(true)
68 | expect(http_client).
69 | to receive(:verify_mode=).
70 | with(OpenSSL::SSL::VERIFY_NONE)
71 | expect(http_client).
72 | to receive(:get).
73 | with(blocked_project_api, expected_headers).
74 | and_return(OpenStruct.new({:code => "404"}))
75 | result = DeliveryTruck::DeliveryApiClient.blocked_projects(node)
76 | expect(result).to eql([])
77 | end
78 | end
79 |
80 | context 'responses' do
81 | before(:each) do
82 | allow(Net::HTTP).
83 | to receive(:new).
84 | with(api_host, api_port).
85 | and_return(http_client)
86 | allow(http_client).
87 | to receive(:use_ssl=).
88 | with(true)
89 | allow(http_client).
90 | to receive(:verify_mode=).
91 | with(OpenSSL::SSL::VERIFY_NONE)
92 | end
93 |
94 | context 'when server returns an error' do
95 | before do
96 | expect(http_client).
97 | to receive(:get).
98 | with(blocked_project_api, expected_headers).
99 | and_return(OpenStruct.new({:code => error_code}))
100 | end
101 |
102 | context 'status 404' do
103 | let(:error_code) { "404" }
104 |
105 | it 'returns empty array' do
106 | result = DeliveryTruck::DeliveryApiClient.blocked_projects(node)
107 | expect(result).to eql([])
108 | end
109 | end
110 |
111 | context 'status not 404' do
112 | let(:error_code) { "500" }
113 |
114 | it 'logs and reraises' do
115 | # Swallow error reporting, to avoid cluttering test output
116 | allow(Chef::Log).
117 | to receive(:error)
118 |
119 | expect{DeliveryTruck::DeliveryApiClient.blocked_projects(node)}.
120 | to raise_exception(DeliveryTruck::DeliveryApiClient::BadApiResponse)
121 | end
122 | end
123 | end
124 |
125 | context 'when request succeeds' do
126 | let(:http_response) { double 'Net::HTTPOK' }
127 | let(:json_response) do
128 | JSON.generate({
129 | 'blocked_projects' => ['project_name_1', 'project_name_2']
130 | })
131 | end
132 |
133 | before do
134 | expect(http_response).
135 | to receive(:body).
136 | and_return(json_response)
137 | allow(http_response).
138 | to receive(:code).
139 | and_return("200")
140 | expect(http_client).
141 | to receive(:get).
142 | with(blocked_project_api, expected_headers).
143 | and_return(http_response)
144 | end
145 |
146 | it 'returns deserialized list' do
147 | result = DeliveryTruck::DeliveryApiClient.blocked_projects(node)
148 | expect(result).to eql(['project_name_1', 'project_name_2'])
149 | end
150 | end
151 | end
152 | end
153 | end
154 |
--------------------------------------------------------------------------------
/spec/unit/recipes/syntax_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "delivery-truck::syntax" do
4 | let(:chef_run) do
5 | ChefSpec::ServerRunner.new do |node|
6 | node.default['delivery']['workspace']['root'] = '/tmp'
7 | node.default['delivery']['workspace']['repo'] = '/tmp/repo'
8 | node.default['delivery']['workspace']['chef'] = '/tmp/chef'
9 | node.default['delivery']['workspace']['cache'] = '/tmp/cache'
10 |
11 | node.default['delivery']['change']['enterprise'] = 'Chef'
12 | node.default['delivery']['change']['organization'] = 'Delivery'
13 | node.default['delivery']['change']['project'] = 'Secret'
14 | node.default['delivery']['change']['pipeline'] = 'master'
15 | node.default['delivery']['change']['change_id'] = 'aaaa-bbbb-cccc'
16 | node.default['delivery']['change']['patchset_number'] = '1'
17 | node.default['delivery']['change']['stage'] = 'union'
18 | node.default['delivery']['change']['phase'] = 'deploy'
19 | node.default['delivery']['change']['git_url'] = 'https://git.co/my_project.git'
20 | node.default['delivery']['change']['sha'] = '0123456789abcdef'
21 | node.default['delivery']['change']['patchset_branch'] = 'mypatchset/branch'
22 | end.converge(described_recipe)
23 | end
24 |
25 | describe 'syntax checks using `knife cookbook test`' do
26 | before do
27 | allow(DeliveryTruck::Helpers::Syntax).to receive(:bumped_version?).and_return(true)
28 | end
29 |
30 | context "when a single cookbook has been modified" do
31 | before do
32 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(one_changed_cookbook)
33 | end
34 |
35 | it "runs `knife cookbook test` against only that cookbook" do
36 | expect(chef_run).to run_execute("syntax_check_julia").with(
37 | :command => "knife cookbook test -o /tmp/repo/cookbooks/julia -a"
38 | )
39 | expect(chef_run).not_to run_execute("syntax_check_gordon")
40 | expect(chef_run).not_to run_execute("syntax_check_emeril")
41 | end
42 | end
43 |
44 | context "when multiple cookbooks have been modified" do
45 | before do
46 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(two_changed_cookbooks)
47 | end
48 |
49 | it "runs `knife cookbook test` against only those cookbooks" do
50 | expect(chef_run).to run_execute("syntax_check_julia").with(
51 | :command => "knife cookbook test -o /tmp/repo/cookbooks/julia -a"
52 | )
53 | expect(chef_run).to run_execute("syntax_check_gordon").with(
54 | :command => "knife cookbook test -o /tmp/repo/cookbooks/gordon -a"
55 | )
56 | expect(chef_run).not_to run_execute("syntax_check_emeril")
57 | end
58 | end
59 |
60 | context "when no cookbooks have been modified" do
61 | before do
62 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(no_changed_cookbooks)
63 | end
64 |
65 | it "does not run `knife cookbook test` against any cookbooks" do
66 | expect(chef_run).not_to run_execute("syntax_check_julia")
67 | expect(chef_run).not_to run_execute("syntax_check_gordon")
68 | expect(chef_run).not_to run_execute("syntax_check_emeril")
69 | end
70 | end
71 | end
72 |
73 | # Temporal Test - Modifying the Release process Stage 1
74 | #
75 | # TODO: Remove this on Stage 2
76 | describe 'temporal test to detect entries in Berksfile' do
77 | before do
78 | allow(DeliveryTruck::Helpers::Syntax).to receive(:bumped_version?).and_return(true)
79 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(one_changed_cookbook)
80 | end
81 |
82 | context 'when there are NO entries' do
83 | it 'logs warning messages' do
84 | expect(chef_run).not_to write_log('warning_delivery-sugar_pull_from_github')
85 | expect(chef_run).not_to write_log('warning_delivery-truck_pull_from_github')
86 | end
87 | end
88 |
89 | context 'when there are wrong entries' do
90 | let(:mock_berksfile) do
91 | < true do
15 | let(:chef_run) { ChefSpec::SoloRunner.converge(described_recipe) }
16 |
17 | before do
18 | allow_any_instance_of(Chef::Recipe).to receive(:load_config).and_return(nil)
19 | allow_any_instance_of(Chef::Recipe).to receive(:repo_path).and_return('/tmp')
20 | end
21 |
22 | context "when a single cookbook has been modified" do
23 | before do
24 | allow_any_instance_of(Chef::Recipe).to receive(:current_stage).and_return('acceptance')
25 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(one_changed_cookbook)
26 | allow(DeliveryTruck::Helpers::Functional).to receive(:has_kitchen_tests?).with('/tmp/cookbooks/julia').and_return(true)
27 | end
28 |
29 | include_examples "cleanup docker"
30 |
31 | it "runs test-kitchen against only that cookbook" do
32 | expect(chef_run).to run_delivery_truck_exec("functional_kitchen_julia").with(
33 | :cwd => "/tmp/cookbooks/julia",
34 | :command => "KITCHEN_YAML=/tmp/cookbooks/julia/.kitchen.docker.yml kitchen test"
35 | )
36 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_gordon")
37 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_emeril")
38 | end
39 | end
40 |
41 | context "when multiple cookbooks have been modified" do
42 | before do
43 | allow_any_instance_of(Chef::Recipe).to receive(:current_stage).and_return('acceptance')
44 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(two_changed_cookbooks)
45 | allow(DeliveryTruck::Helpers::Functional).to receive(:has_kitchen_tests?).with('/tmp/cookbooks/julia').and_return(true)
46 | allow(DeliveryTruck::Helpers::Functional).to receive(:has_kitchen_tests?).with('/tmp/cookbooks/gordon').and_return(true)
47 | end
48 |
49 | include_examples "cleanup docker"
50 |
51 | it "runs test-kitchen against only those cookbooks" do
52 | expect(chef_run).to run_delivery_truck_exec("functional_kitchen_julia").with(
53 | :cwd => "/tmp/cookbooks/julia",
54 | :command => "KITCHEN_YAML=/tmp/cookbooks/julia/.kitchen.docker.yml kitchen test"
55 | )
56 | expect(chef_run).to run_delivery_truck_exec("functional_kitchen_gordon").with(
57 | :cwd => "/tmp/cookbooks/gordon",
58 | :command => "KITCHEN_YAML=/tmp/cookbooks/gordon/.kitchen.docker.yml kitchen test"
59 | )
60 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_emeril")
61 | end
62 |
63 | context "but a cookbook has no tests" do
64 | before do
65 | allow(DeliveryTruck::Helpers::Functional).to receive(:has_kitchen_tests?).with('/tmp/cookbooks/gordon').and_return(false)
66 | end
67 |
68 | include_examples "cleanup docker"
69 |
70 | it "skips that cookbook" do
71 | expect(chef_run).to run_delivery_truck_exec("functional_kitchen_julia").with(
72 | :cwd => "/tmp/cookbooks/julia",
73 | :command => "KITCHEN_YAML=/tmp/cookbooks/julia/.kitchen.docker.yml kitchen test"
74 | )
75 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_gordon")
76 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_emeril")
77 | end
78 | end
79 | end
80 |
81 | context "when no cookbooks have been modified" do
82 | before do
83 | allow_any_instance_of(Chef::Recipe).to receive(:current_stage).and_return('acceptance')
84 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks).and_return(no_changed_cookbooks)
85 | end
86 |
87 | it "does not run test-kitchen against any cookbooks" do
88 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_julia")
89 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_gordon")
90 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_emeril")
91 | end
92 | end
93 |
94 | context 'non-acceptance environments' do
95 | before do
96 | allow_any_instance_of(Chef::Recipe).to receive(:current_stage).and_return('union')
97 | end
98 |
99 | it 'does nothing' do
100 | expect(chef_run).not_to run_execute('stop_all_docker_containers')
101 | expect(chef_run).not_to run_execute('kill_all_docker_containers')
102 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_julia")
103 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_gordon")
104 | expect(chef_run).not_to run_delivery_truck_exec("functional_kitchen_emeril")
105 | end
106 | end
107 | end
108 |
--------------------------------------------------------------------------------
/spec/unit/libraries/helpers_syntax_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module DeliverySugar
4 | class Change
5 | end
6 | end
7 |
8 | describe DeliveryTruck::Helpers::Syntax do
9 |
10 | describe '.bumped_version?' do
11 | let(:node) { double("node") }
12 | let(:workspace) { '/tmp/repo' }
13 | let(:pipeline) { 'master' }
14 | let(:relative_path) { '.' }
15 |
16 | let(:base_version) { '0.0.1' }
17 | let(:base_metadata) { double('metadata', name: 'julia', version: base_version) }
18 |
19 | let(:current_version) { '0.0.1' }
20 | let(:current_metadata) { double('metadata', name: 'julia', version: current_version) }
21 |
22 | let(:sugar_change) { double('delivery sugar change',
23 | workspace_repo: workspace,
24 | changed_files: changed_files,
25 | pipeline: pipeline,
26 | merge_sha: merge_sha) }
27 |
28 | before do
29 | allow(DeliverySugar::Change).to receive(:new).and_return(sugar_change)
30 | allow(sugar_change).to receive(:cookbook_metadata)
31 | .with(File.expand_path(relative_path, workspace)).and_return(current_metadata)
32 | end
33 |
34 | context 'with an unmerged change' do
35 | let(:merge_sha) { '' }
36 |
37 | before do
38 | allow(sugar_change).to receive(:cookbook_metadata)
39 | .with(File.expand_path(relative_path, workspace), 'origin/master').and_return(base_metadata)
40 | end
41 |
42 | context 'when root cookbook was updated' do
43 | let(:changed_files) { ['README.md', 'recipes/default.rb', 'metadata.rb'] }
44 |
45 | context 'without version bump' do
46 | let(:current_version) { '0.0.1' }
47 |
48 | it 'returns false' do
49 | expect(described_class.bumped_version?(workspace, node)).to eql false
50 | end
51 | end
52 |
53 | context 'with version bump' do
54 | let(:current_version) { '0.0.2' }
55 |
56 | it 'returns true' do
57 | expect(described_class.bumped_version?(workspace, node)).to eql true
58 | end
59 | end
60 | end
61 |
62 | context 'when non-cookbook file in root cookbook was updated' do
63 | let(:changed_files) { ['README.md'] }
64 |
65 | it 'returns true' do
66 | expect(described_class.bumped_version?(workspace, node)).to eql true
67 | end
68 | end
69 |
70 | context 'when non-cookbook file in cookbooks directory was updated' do
71 | let(:changed_files) { ['cookbooks/julia/README.md'] }
72 | let(:relative_path) { 'cookbooks/julia' }
73 |
74 | it 'returns true' do
75 | expect(described_class.bumped_version?(workspace, node)).to eql true
76 | end
77 | end
78 |
79 | context 'when cookbook in cookbooks directory was updated' do
80 | let(:changed_files) { ['cookbooks/julia/README.md', 'cookbooks/julia/recipes/default.rb', 'cookbooks/julia/metadata.rb'] }
81 | let(:relative_path) { 'cookbooks/julia' }
82 |
83 | context 'without version bump' do
84 | let(:current_version) { '0.0.1' }
85 |
86 | it 'returns false' do
87 | expect(described_class.bumped_version?("#{workspace}/#{relative_path}", node)).to eql false
88 | end
89 | end
90 |
91 | context 'with version bump' do
92 | let(:current_version) { '0.0.2' }
93 |
94 | it 'returns true' do
95 | expect(described_class.bumped_version?("#{workspace}/#{relative_path}", node)).to eql true
96 | end
97 | end
98 | end
99 | end
100 |
101 | context 'with a merged change' do
102 | let(:merge_sha) { 'abcdfakefake' }
103 |
104 | before do
105 | allow(sugar_change).to receive(:cookbook_metadata)
106 | .with(File.expand_path(relative_path, workspace), 'abcdfakefake~1').and_return(base_metadata)
107 | end
108 |
109 | context 'when root cookbook was updated' do
110 | let(:changed_files) { ['README.md', 'recipes/default.rb', 'metadata.rb'] }
111 |
112 | context 'without version bump' do
113 | let(:current_version) { '0.0.1' }
114 |
115 | it 'returns false' do
116 | expect(described_class.bumped_version?(workspace, node)).to eql false
117 | end
118 | end
119 |
120 | context 'with version bump' do
121 | let(:current_version) { '0.0.2' }
122 |
123 | it 'returns true' do
124 | expect(described_class.bumped_version?(workspace, node)).to eql true
125 | end
126 | end
127 | end
128 |
129 | context 'when non-cookbook file in root cookbook was updated' do
130 | let(:changed_files) { ['README.md'] }
131 |
132 | it 'returns true' do
133 | expect(described_class.bumped_version?(workspace, node)).to eql true
134 | end
135 | end
136 |
137 | context 'when non-cookbook file in cookbooks directory was updated' do
138 | let(:changed_files) { ['cookbooks/julia/README.md'] }
139 | let(:relative_path) { 'cookbooks/julia' }
140 |
141 | it 'returns true' do
142 | expect(described_class.bumped_version?(workspace, node)).to eql true
143 | end
144 | end
145 |
146 | context 'when cookbook in cookbooks directory was updated' do
147 | let(:changed_files) { ['cookbooks/julia/README.md', 'cookbooks/julia/recipes/default.rb', 'cookbooks/julia/metadata.rb'] }
148 | let(:relative_path) { 'cookbooks/julia' }
149 |
150 | context 'without version bump' do
151 | let(:current_version) { '0.0.1' }
152 |
153 | it 'returns false' do
154 | expect(described_class.bumped_version?("#{workspace}/#{relative_path}", node)).to eql false
155 | end
156 | end
157 |
158 | context 'with version bump' do
159 | let(:current_version) { '0.0.2' }
160 |
161 | it 'returns true' do
162 | expect(described_class.bumped_version?("#{workspace}/#{relative_path}", node)).to eql true
163 | end
164 | end
165 | end
166 | end
167 | end
168 | end
169 |
--------------------------------------------------------------------------------
/spec/unit/libraries/helpers_lint_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DeliveryTruck::Helpers::Lint do
4 | let(:node) { Chef::Node.new }
5 |
6 | describe '.foodcritic_tags' do
7 | context 'when foodcritic config is nil' do
8 | before do
9 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic'] = nil
10 | end
11 |
12 | it 'ignores issues_url and source_url rules by default' do
13 | expect(described_class.foodcritic_tags(node)).to eql "-t ~FC064 -t ~FC065"
14 | end
15 | end
16 |
17 | context 'when cookbook will be share to supermarket' do
18 | before do
19 | node.default['delivery']['config']['delivery-truck']['publish']['supermarket'] = 'supermarket.chef.io'
20 | end
21 |
22 | it 'does not ignores issues_url and source_url rules' do
23 | expect(described_class.foodcritic_tags(node)).to eql ""
24 | end
25 | end
26 |
27 | context 'when foodcritic config is empty' do
28 | before do
29 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic'] = {}
30 | end
31 |
32 | it 'ignores issues_url and source_url rules by default' do
33 | expect(described_class.foodcritic_tags(node)).to eql "-t ~FC064 -t ~FC065"
34 | end
35 | end
36 |
37 | context 'when `only_rules` has been set' do
38 | context 'with no rules' do
39 | before do
40 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['only_rules'] = []
41 | end
42 |
43 | it 'ignores issues_url and source_url rules by default' do
44 | expect(described_class.foodcritic_tags(node)).to eql "-t ~FC064 -t ~FC065"
45 | end
46 | end
47 |
48 | context 'with one rule' do
49 | before do
50 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['only_rules'] = ['FC001']
51 | end
52 |
53 | it 'returns a string with the one rule' do
54 | expect(described_class.foodcritic_tags(node)).to eql "-t FC001"
55 | end
56 | end
57 |
58 | context 'with multiple rules' do
59 | before do
60 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['only_rules'] = ['FC001', 'FC002']
61 | end
62 |
63 | it 'returns a string with multiple rules' do
64 | expect(described_class.foodcritic_tags(node)).to eql "-t FC001,FC002"
65 | end
66 | end
67 | end
68 |
69 | context 'when `ignore_rules` has been set' do
70 | context 'with no rules' do
71 | before do
72 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['ignore_rules'] = []
73 | end
74 |
75 | it 'ignores issues_url and source_url rules by default' do
76 | expect(described_class.foodcritic_tags(node)).to eql ""
77 | end
78 | end
79 |
80 | context 'with one rule' do
81 | before do
82 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['ignore_rules'] = ['FC001']
83 | end
84 |
85 | it 'returns a string with the one rule' do
86 | expect(described_class.foodcritic_tags(node)).to eql "-t ~FC001"
87 | end
88 | end
89 |
90 | context 'with multiple rules' do
91 | before do
92 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['ignore_rules'] = ['FC001', 'FC002']
93 | end
94 |
95 | it 'returns a string with multiple rules' do
96 | expect(described_class.foodcritic_tags(node)).to eql "-t ~FC001 -t ~FC002"
97 | end
98 | end
99 | end
100 |
101 | context 'when `only_rules` and `ignore_rules` have both been set' do
102 | before do
103 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['only_rules'] = ['FC001']
104 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['ignore_rules'] = ['FC002']
105 | end
106 |
107 | it 'only `only_rules` values are honored' do
108 | expect(described_class.foodcritic_tags(node)). to eql "-t FC001"
109 | end
110 | end
111 |
112 | context 'when `exclude` has been set' do
113 | context 'with no paths' do
114 | before do
115 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['excludes'] = []
116 | end
117 |
118 | it 'returns an empty string' do
119 | expect(described_class.foodcritic_excludes(node)).to eql ""
120 | end
121 | end
122 |
123 | context 'with one path' do
124 | before do
125 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['excludes'] = ['spec']
126 | end
127 |
128 | it 'returns a string with the one exclude' do
129 | expect(described_class.foodcritic_excludes(node)).to eql "--exclude spec"
130 | end
131 | end
132 |
133 | context 'with multiple paths' do
134 | before do
135 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['excludes'] = ['spec', 'test']
136 | end
137 |
138 | it 'returns a string with multiple excludes' do
139 | expect(described_class.foodcritic_excludes(node)).to eql "--exclude spec --exclude test"
140 | end
141 | end
142 | end
143 |
144 | context 'when `fail_tags` has been set' do
145 | context 'with no rules' do
146 | before do
147 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['fail_tags'] = []
148 | end
149 |
150 | it 'returns correctness tag' do
151 | expect(described_class.foodcritic_fail_tags(node)).to eql '-f correctness'
152 | end
153 | end
154 |
155 | context 'with one rule' do
156 | before do
157 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['fail_tags'] = ['any']
158 | end
159 |
160 | it 'returns a string with the one rule' do
161 | expect(described_class.foodcritic_fail_tags(node)).to eql '-f any'
162 | end
163 | end
164 |
165 | context 'with multiple rules' do
166 | before do
167 | node.default['delivery']['config']['delivery-truck']['lint']['foodcritic']['fail_tags'] = ['correctness', 'metadata']
168 | end
169 |
170 | it 'returns a string with multiple rules' do
171 | expect(described_class.foodcritic_fail_tags(node)).to eql "-f correctness,metadata"
172 | end
173 | end
174 | end
175 | end
176 | end
177 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Umbrella Project**: [Automate](https://github.com/chef/chef-oss-practices/blob/master/projects/chef-automate.md)
2 |
3 | **Project State**: [Deprecated](https://github.com/chef/chef-oss-practices/blob/master/repo-management/repo-states.md#deprecated)
4 |
5 | **Issues [Response Time Maximum](https://github.com/chef/chef-oss-practices/blob/master/repo-management/repo-states.md)**: None
6 |
7 | **Pull Request [Response Time Maximum](https://github.com/chef/chef-oss-practices/blob/master/repo-management/repo-states.md)**: None
8 |
9 | # `delivery-truck`
10 | `delivery-truck` is a Chef Delivery build_cookbook for continuously delivering
11 | Chef cookbooks and applications.
12 |
13 | To quickly get started you just need to set `delivery-truck` to
14 | be your build cookbook in your `.delivery/config.json`.
15 |
16 | ```
17 | {
18 | "version": "2",
19 | "build_cookbook": {
20 | "name": "delivery-truck",
21 | "git": "https://github.com/chef-cookbooks/delivery-truck.git"
22 | }
23 | }
24 | ```
25 |
26 | ## Customizing Behavior using `.delivery/config.json`
27 | The behavior of the `delivery-truck` cookbook phase recipes can be easily
28 | controlled by specifying certain values in your `.delivery/config.json` file.
29 | The control these values offer you is limited and not meant as a method to
30 | drastically alter the way the recipe functions.
31 |
32 | ### lint
33 | The `lint` phase will execute [foodcritic](http://foodcritic.io) but you can specify
34 | which rules you would like to follow directly from your `config.json`.
35 |
36 | * `ignore_rules` - Provide a list of foodcritic rules you would like to ignore.
37 | * `only_rules` - Explicitly state which foodcritic rules you would like to run.
38 | Any other rules except these will be ignored.
39 | * `excludes` - Explicitly state which relative paths foodcritic should ignore.
40 | * `fail_tags` - Explicitly state which rules should cause the run to fail. Defaults
41 | to `correctness`.
42 |
43 | ```json
44 | {
45 | "version": "2",
46 | "build_cookbook": {
47 | "name": "delivery-truck",
48 | "git": "https://github.com/chef-cookbooks/delivery-truck.git"
49 | },
50 | "delivery-truck": {
51 | "lint": {
52 | "foodcritic": {
53 | "ignore_rules": ["FC001"],
54 | "only_rules": ["FC002"],
55 | "excludes": ["spec", "test"],
56 | "fail_tags": ["any"]
57 | }
58 | }
59 | }
60 | }
61 | ```
62 |
63 | By default, the `lint` phase will run [RuboCop](http://batsov.com/rubocop/), but
64 | only on cookbooks that have a `.rubocop.yml` file.
65 |
66 | You can over-ride this behavior to use [cookstyle](https://github.com/chef/cookstyle)
67 | instead of RuboCop by enabling it in your `config.json`.
68 |
69 | ```json
70 | {
71 | "version": "2",
72 | "build_cookbook": {
73 | "name": "delivery-truck",
74 | "git": "https://github.com/chef-cookbooks/delivery-truck.git"
75 | },
76 | "delivery-truck": {
77 | "lint": {
78 | "enable_cookstyle": true
79 | }
80 | }
81 | }
82 | ```
83 |
84 | *Note: To enable cookstyle, your builders/runners must be running ChefDK version
85 | v0.14 or higher.*
86 |
87 | ### publish
88 | From the `publish` phase you can quickly and easily deploy cookbooks to
89 | your Chef Server, Supermarket Server and your entire project to a Github account.
90 |
91 | * `chef_server` - Set to true/false depending on whether you would like to
92 | upload any modified cookbooks to the Chef Server associated with Delivery.
93 | * `supermarket` - Specify the Supermarket Server you would like to use to
94 | share any modified cookbooks.
95 | * `github` - Specify the Github repository you would like to push your project
96 | to. In order to work you must create a shared secrets data bag item (see "Handling
97 | Secrets" below) with a key named github with the value being a
98 | [deploy key](https://developer.github.com/guides/managing-deploy-keys/) with
99 | access to that repo.
100 | * `git` - Same as `github` but for Open Source Git Servers. (The data bag item
101 | should have a key named git)
102 |
103 | ```json
104 | {
105 | "version": "2",
106 | "build_cookbook": {
107 | "name": "delivery-truck",
108 | "git": "https://github.com/chef-cookbooks/delivery-truck.git"
109 | },
110 | "delivery-truck": {
111 | "publish": {
112 | "chef_server": true,
113 | "supermarket": "https://supermarket.chef.io",
114 | "github": "/",
115 | "git": "ssh://git@stash:2222//"
116 | }
117 | }
118 | }
119 | ```
120 |
121 | *example data bag*
122 | ```json
123 | {
124 | "id": "",
125 | "github": "",
126 | "git": ""
127 | }
128 | ```
129 |
130 | ### deploy
131 | By default deploy will trigger a `chef-client` run through push-jobs to all
132 | the nodes that belong to the current environment in delivery and have the
133 | modified cookbook(s) in their run_list. You can customize the search query.
134 |
135 | ```json
136 | {
137 | "delivery-truck": {
138 | "deploy": {
139 | "search": "recipes:my_push_jobs"
140 | }
141 | }
142 | }
143 | ```
144 |
145 | ## Skipped Phases
146 | The following phases have no content and can be skipped: functional,
147 | quality, security and smoke.
148 |
149 | ```json
150 | {
151 | "version": "2",
152 | "build_cookbook": {
153 | "name": "delivery-truck",
154 | "git": "https://github.com/chef-cookbooks/delivery-truck.git"
155 | },
156 | "skip_phases": [
157 | "functional",
158 | "quality",
159 | "security",
160 | "smoke"
161 | ]
162 | }
163 | ```
164 |
165 | ## Depends on delivery-truck
166 | If you would like to enjoy all the functionalities that `delivery-truck` provides
167 | on you own build cookbook you need to add it into your `metadata.rb`
168 |
169 | ```
170 | name 'build_cookbook'
171 | maintainer 'The Authors'
172 | maintainer_email 'you@example.com'
173 | license 'all_rights'
174 | description 'Installs/Configures build'
175 | long_description 'Installs/Configures build'
176 | version '0.1.0'
177 |
178 | depends 'delivery-truck'
179 |
180 | ```
181 |
182 | Additionally `delivery-truck` depends on `delivery-sugar` so you need to add
183 | them both to your `Berksfile`
184 |
185 | ```
186 | source "https://supermarket.chef.io"
187 |
188 | metadata
189 |
190 | cookbook 'delivery-truck', github: 'chef-cookbooks/delivery-truck'
191 | cookbook 'delivery-sugar', github: 'chef-cookbooks/delivery-sugar'
192 |
193 | ```
194 |
195 | ## Handling Secrets (ALPHA)
196 | This cookbook implements a rudimentary approach to handling secrets. This process
197 | is largely out of band from Chef Delivery for the time being.
198 |
199 | `delivery-truck` will look for secrets in the `delivery-secrets` data bag on the
200 | Delivery Chef Server. It will expect to find an item in that data bag named
201 | `--`. For example, this cookbook is kept in the
202 | 'Delivery-Build-Cookbooks' org of the 'chef' enterprise so it's data bag name is
203 | `chef-Delivery-Build-Cookbooks-delivery-truck`.
204 |
205 | This cookbook expects this data bag item to be encrypted with the same
206 | encrypted_data_bag_secret that is on your builders. You will need to ensure that
207 | the data bag is available on the Chef Server before you run this cookbook for
208 | the first time otherwise it will fail.
209 |
210 | To get this data bag you can use the DSL `get_project_secrets` to get the
211 | contents of the data bag.
212 |
213 | ```
214 | my_secrets = get_project_secrets
215 | puts my_secrets['id'] # chef-Delivery-Build-Cookbooks-delivery-truck
216 | ```
217 |
218 | ## License & Authors
219 | - Author:: Tom Duffield ()
220 | - Author:: Salim Afiune ()
221 |
222 | ```text
223 | Copyright:: 2015 Chef Software, Inc
224 |
225 | Licensed under the Apache License, Version 2.0 (the "License");
226 | you may not use this file except in compliance with the License.
227 | You may obtain a copy of the License at
228 |
229 | http://www.apache.org/licenses/LICENSE-2.0
230 |
231 | Unless required by applicable law or agreed to in writing, software
232 | distributed under the License is distributed on an "AS IS" BASIS,
233 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
234 | See the License for the specific language governing permissions and
235 | limitations under the License.
236 | ```
237 |
--------------------------------------------------------------------------------
/libraries/delivery_truck_deploy.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | class Chef
19 | class Provider
20 | class DeliveryTruckDeploy < Chef::Provider::LWRPBase
21 | action :run do
22 | converge_by("Dispatch push-job for #{delivery_environment} => #{node['delivery']['change']['project']} - #{new_resource.name}") do
23 | result = with_server_config { deploy_ccr }
24 | new_resource.updated_by_last_action(result)
25 | end
26 | end
27 |
28 | private
29 |
30 | SLEEP_TIME ||= 15
31 | PUSH_SLEEP_TIME ||= 5
32 |
33 | def get_search
34 | @search ||= begin
35 | # Our default search has to be here since we evaluate `project_name`
36 | #
37 | # This search is designed to include all the nodes that in the expanded
38 | # run_list have the "project_name". This will apply the majority of the
39 | # times with cookbooks that doesn't have a secial sausage like:
40 | # => `my_app_cookbook:deploy_db`
41 | #
42 | # If this is a project like delivery that the app_name and the
43 | # project_name are totally different from the deploy_cookbook you
44 | # can customize the search.
45 | (@new_resource.search || "recipes:#{node['delivery']['change']['project']}*").tap do |search|
46 | # We validate that the user has provided a chef_environment
47 | search << " AND chef_environment:#{delivery_environment}" unless search =~ /chef_environment/
48 |
49 | # We will search only on nodes that has push-jobs
50 | search << " AND recipes:push-jobs*"
51 | end
52 | end
53 | end
54 |
55 | def timeout
56 | @timeout ||= new_resource.timeout
57 | end
58 |
59 | def dec_timeout(number)
60 | @timeout -= number
61 | end
62 |
63 | def deploy_ccr
64 | origin = timeout
65 |
66 | ::Chef::Log.info("Will wait up to #{timeout/60} minutes for " +
67 | "deployment to complete...")
68 |
69 | begin
70 | # Sleep unless this is our first time through the loop.
71 | sleep(SLEEP_TIME) unless timeout == origin
72 |
73 | # Find any dependency/app node
74 | ::Chef::Log.info("Finding dependency/app nodes in #{delivery_environment}...")
75 | nodes = search(:node, get_search)
76 |
77 | if !nodes || nodes.empty?
78 | # We didn't find any node to deploy. Lets skip this phase!
79 | ::Chef::Log.info("No dependency/app nodes found. Skipping phase!")
80 | break
81 | end
82 |
83 | node_names = nodes.map { |n| n.name }
84 |
85 | # We take out the build node we are running on
86 | node_names.delete(node.name)
87 |
88 | ::Chef::Log.info("Found dependency/app nodes: #{node_names}")
89 |
90 | chef_server_rest = Chef::REST.new(Chef::Config[:chef_server_url])
91 |
92 | # Kick off command via push.
93 | ::Chef::Log.info("Triggering #{new_resource.command} on dependency nodes " +
94 | "with Chef Push Jobs...")
95 |
96 | req = {
97 | 'command' => new_resource.command,
98 | 'nodes' => node_names
99 | }
100 | resp = chef_server_rest.post('/pushy/jobs', req)
101 | job_uri = resp['uri']
102 |
103 | unless job_uri
104 | # We were not able to start the push job.
105 | ::Chef::Log.info("Could not start push job. " +
106 | "Will try again in #{SLEEP_TIME} seconds...")
107 | next
108 | end
109 |
110 | ::Chef::Log.info("Started push job with id: #{job_uri[-32,32]}")
111 | previous_state = "initialized"
112 | begin
113 | sleep(PUSH_SLEEP_TIME) unless previous_state == "initialized"
114 | job = chef_server_rest.get_rest(job_uri)
115 | case job['status']
116 | when 'new'
117 | finished = false
118 | state = 'initialized'
119 | when 'voting'
120 | finished = false
121 | state = job['status']
122 | else
123 | total = job['nodes'].values.inject(0) do |sum, n|
124 | sum + n.length
125 | end
126 |
127 | in_progress = job['nodes'].keys.inject(0) do |sum, status|
128 | nodes = job['nodes'][status]
129 | sum + (%w(new voting running).include?(status) ? 1 : 0)
130 | end
131 |
132 | if job['status'] == 'running'
133 | finished = false
134 | state = job['status'] +
135 | " (#{in_progress}/#{total} in progress) ..."
136 | else
137 | finished = true
138 | state = job['status']
139 | end
140 | end
141 | if state != previous_state
142 | ::Chef::Log.info("Push Job Status: #{state}")
143 | previous_state = state
144 | end
145 |
146 | ## Check for success
147 | if finished && job['nodes']['succeeded'] &&
148 | job['nodes']['succeeded'].size == nodes.size
149 | ::Chef::Log.info("Deployment complete in " +
150 | "#{(origin-timeout)/60} minutes. " +
151 | "Deploy Successful!")
152 | break
153 | elsif finished == true && job['nodes']['failed'] || job['nodes']['unavailable']
154 | ::Chef::Log.info("Deployment failed on the following nodes with status: ")
155 | ::Chef::Log.info(" => Failed: #{job['nodes']['failed']}.") if job['nodes']['failed']
156 | ::Chef::Log.info(" => Unavailable: #{job['nodes']['unavailable']}.") if job['nodes']['unavailable']
157 | raise "Deployment failed! Not all nodes were successful."
158 | end
159 |
160 | dec_timeout(PUSH_SLEEP_TIME)
161 | end until timeout <= 0
162 |
163 | break if finished
164 |
165 | ## If we make it here and we are past our timeout the job timed out
166 | ## waiting for the push job.
167 | if timeout <= 0
168 | ::Chef::Log.error("Timed out after #{origin/60} minutes waiting "+
169 | "for push job. Deploy Failed...")
170 | raise "Timeout waiting for deploy..."
171 | end
172 |
173 | dec_timeout(SLEEP_TIME)
174 | end while timeout > 0
175 |
176 | ## If we make it here and we are past our timeout the job timed out.
177 | if timeout <= 0
178 | ::Chef::Log.error("Timed out after #{origin/60} minutes waiting "+
179 | "for deployment to complete. Deploy Failed...")
180 | raise "Timeout waiting for deploy..."
181 | end
182 |
183 | # we survived
184 | true
185 | end
186 | end
187 | end
188 | end
189 |
190 | class Chef
191 | class Resource
192 | class DeliveryTruckDeploy < Chef::Resource::LWRPBase
193 | actions :run
194 |
195 | default_action :run
196 |
197 | attribute :command, :kind_of => String, :default => 'chef-client'
198 | attribute :timeout, :kind_of => Integer, :default => 30 * 60 # 30 mins
199 | attribute :search, :kind_of => String
200 |
201 | self.resource_name = :delivery_truck_deploy
202 | def initialize(name, run_context=nil)
203 | super
204 | @provider = Chef::Provider::DeliveryTruckDeploy
205 | end
206 | end
207 | end
208 | end
209 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/spec/unit/recipes/publish_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "delivery-truck::publish" do
4 | let(:chef_run) do
5 | ChefSpec::SoloRunner.new do |node|
6 | node.default['delivery']['workspace']['root'] = '/tmp'
7 | node.default['delivery']['workspace']['repo'] = '/tmp/repo'
8 | node.default['delivery']['workspace']['chef'] = '/tmp/chef'
9 | node.default['delivery']['workspace']['cache'] = '/tmp/cache'
10 |
11 | node.default['delivery']['change']['enterprise'] = 'Chef'
12 | node.default['delivery']['change']['organization'] = 'Delivery'
13 | node.default['delivery']['change']['project'] = 'Secret'
14 | node.default['delivery']['change']['pipeline'] = 'master'
15 | node.default['delivery']['change']['change_id'] = 'aaaa-bbbb-cccc'
16 | node.default['delivery']['change']['patchset_number'] = '1'
17 | node.default['delivery']['change']['stage'] = 'acceptance'
18 | node.default['delivery']['change']['phase'] = 'publish'
19 | node.default['delivery']['change']['git_url'] = 'https://git.co/my_project.git'
20 | node.default['delivery']['change']['sha'] = '0123456789abcdef'
21 | node.default['delivery']['change']['patchset_branch'] = 'mypatchset/branch'
22 | end
23 | end
24 |
25 | let(:delivery_chef_server) do
26 | {
27 | chef_server_url: 'http://myserver.chef',
28 | options: {
29 | client_name: 'spec',
30 | signing_key_filename: '/tmp/keys/spec.pem'
31 | }
32 | }
33 | end
34 |
35 | before do
36 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks)
37 | .and_return(no_changed_cookbooks)
38 | allow_any_instance_of(Chef::Recipe).to receive(:delivery_chef_server)
39 | .and_return(delivery_chef_server)
40 | end
41 |
42 | context 'always' do
43 | before do
44 | chef_run.converge(described_recipe)
45 | end
46 |
47 | it 'deletes and recreates cookbook staging directory' do
48 | expect(chef_run).to delete_directory("/tmp/cache/cookbook-upload")
49 | .with(recursive: true)
50 | expect(chef_run).to create_directory("/tmp/cache/cookbook-upload")
51 | end
52 | end
53 |
54 | context 'when user does not specify they wish to share to Supermarket Server' do
55 | before do
56 | allow(DeliveryTruck::Helpers::Publish).to receive(:share_cookbook_to_supermarket?)
57 | .and_return(false)
58 | chef_run.converge(described_recipe)
59 | end
60 |
61 | it 'does not share cookbooks' do
62 | expect(chef_run).not_to share_delivery_supermarket('share_julia_to_supermarket')
63 | expect(chef_run).not_to share_delivery_supermarket('share_gordon_to_supermarket')
64 | expect(chef_run).not_to share_delivery_supermarket('share_emeril_to_supermarket')
65 | end
66 | end
67 |
68 | context 'when user specifies they wish to share to Supermarket Server' do
69 | before do
70 | allow(DeliveryTruck::Helpers::Publish).to receive(:share_cookbook_to_supermarket?)
71 | .and_return(true)
72 | chef_run.node.default['delivery']['config']['delivery-truck']['publish'][
73 | 'supermarket'
74 | ] = 'https://supermarket.chef.io'
75 | end
76 |
77 | shared_examples_for 'properly working supermarket upload' do
78 | context 'and no cookbooks changed' do
79 | before do
80 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks)
81 | .and_return(no_changed_cookbooks)
82 | chef_run.converge(described_recipe)
83 | end
84 |
85 | it 'does nothing' do
86 | expect(chef_run).not_to share_delivery_supermarket('share_julia_to_supermarket')
87 | expect(chef_run).not_to share_delivery_supermarket('share_gordon_to_supermarket')
88 | expect(chef_run).not_to share_delivery_supermarket('share_emeril_to_supermarket')
89 | end
90 | end
91 |
92 | context 'and one cookbook changed' do
93 | before do
94 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks)
95 | .and_return(one_changed_cookbook)
96 | chef_run.converge(described_recipe)
97 | end
98 |
99 | it 'shares only that cookbook' do
100 | expect(chef_run).to share_delivery_supermarket('share_julia_to_supermarket')
101 | .with_path('/tmp/repo/cookbooks/julia')
102 | .with_cookbook('julia')
103 | .with_version('0.1.0')
104 | expect(chef_run).not_to share_delivery_supermarket('share_gordon_to_supermarket')
105 | .with_path('/tmp/repo/cookbooks/gordon')
106 | .with_cookbook('gordon')
107 | .with_version('0.2.0')
108 | expect(chef_run).not_to share_delivery_supermarket('share_emeril_to_supermarket')
109 | end
110 | end
111 |
112 | context 'and multiple cookbooks changed' do
113 | before do
114 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks)
115 | .and_return(two_changed_cookbooks)
116 | chef_run.converge(described_recipe)
117 | end
118 |
119 | it 'shares only those cookbook' do
120 | expect(chef_run).to share_delivery_supermarket('share_julia_to_supermarket')
121 | .with_path('/tmp/repo/cookbooks/julia')
122 | .with_cookbook('julia')
123 | .with_version('0.1.0')
124 | expect(chef_run).to share_delivery_supermarket('share_gordon_to_supermarket')
125 | .with_path('/tmp/repo/cookbooks/gordon')
126 | .with_cookbook('gordon')
127 | .with_version('0.2.0')
128 | expect(chef_run).not_to share_delivery_supermarket('share_emeril_to_supermarket')
129 | end
130 | end
131 | end
132 |
133 | context 'when supermarket-custom-credentials is not specified' do
134 | let(:expected_extra_args) { '' }
135 |
136 | it_behaves_like 'properly working supermarket upload'
137 | end
138 |
139 | context 'when supermarket-custom-credentials is specified' do
140 | before do
141 | chef_run.node.default['delivery']['config']['delivery-truck']['publish'][
142 | 'supermarket-custom-credentials'
143 | ] = true
144 | allow_any_instance_of(Chef::Recipe).to receive(:get_project_secrets)
145 | .and_return(secrets)
146 | end
147 |
148 | context 'when secrets are properly set' do
149 | let(:supermarket_tmp_path) { '/tmp/cache/supermarket.pem' }
150 | let(:secrets) do
151 | {
152 | 'supermarket_user' => 'test-user',
153 | 'supermarket_key' => 'test-key',
154 | }
155 | end
156 |
157 | let(:expected_extra_args) { " -u test-user -k #{supermarket_tmp_path}" }
158 |
159 | before do
160 | file = instance_double('File')
161 | allow(File).to receive(:new).with(supermarket_tmp_path, 'w+')
162 | .and_return(file)
163 | allow(file).to receive(:write).with('test-key')
164 | allow(file).to receive(:close)
165 | end
166 |
167 | it_behaves_like 'properly working supermarket upload'
168 |
169 | context 'we pass the user and key to the delivery_supermarket resource' do
170 | before do
171 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks)
172 | .and_return(two_changed_cookbooks)
173 | chef_run.converge(described_recipe)
174 | end
175 |
176 | it 'shares only those cookbook' do
177 | expect(chef_run).to share_delivery_supermarket('share_julia_to_supermarket')
178 | .with_path('/tmp/repo/cookbooks/julia')
179 | .with_cookbook('julia')
180 | .with_version('0.1.0')
181 | .with_user('test-user')
182 | .with_key('test-key')
183 | expect(chef_run).to share_delivery_supermarket('share_gordon_to_supermarket')
184 | .with_path('/tmp/repo/cookbooks/gordon')
185 | .with_cookbook('gordon')
186 | .with_version('0.2.0')
187 | .with_user('test-user')
188 | .with_key('test-key')
189 | expect(chef_run).not_to share_delivery_supermarket('share_emeril_to_supermarket')
190 | end
191 | end
192 | end
193 |
194 | context 'when secrets are missing' do
195 | before do
196 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks)
197 | .and_return(two_changed_cookbooks)
198 | end
199 |
200 | context 'when supermarket_user is not specified in secrets' do
201 | let(:secrets) do
202 | {
203 | 'supermarket_key' => 'test-key'
204 | }
205 | end
206 |
207 | it 'rasies an error' do
208 | expect { chef_run.converge(described_recipe) }.to raise_error(RuntimeError)
209 | end
210 |
211 | end
212 |
213 | context 'when supermarket_user is not specified in secrets' do
214 | let(:secrets) do
215 | {
216 | 'supermarket_user' => 'test-user'
217 | }
218 | end
219 |
220 | it 'rasies an error' do
221 | expect { chef_run.converge(described_recipe) }.to raise_error(RuntimeError)
222 | end
223 |
224 | end
225 | end
226 | end
227 | end
228 |
229 | context 'when user does not specify they wish to upload to Chef Server' do
230 | before do
231 | allow(DeliveryTruck::Helpers::Publish).to receive(:upload_cookbook_to_chef_server?)
232 | .and_return(false)
233 | chef_run.converge(described_recipe)
234 | end
235 |
236 | it 'does not upload cookbooks' do
237 | expect(chef_run).not_to create_link('/tmp/cache/cookbook-upload/julia')
238 | expect(chef_run).not_to create_link('/tmp/cache/cookbook-upload/gordon')
239 | expect(chef_run).not_to create_link('/tmp/cache/cookbook-upload/emeril')
240 |
241 | expect(chef_run).not_to run_execute("upload_cookbook_julia")
242 | expect(chef_run).not_to run_execute("upload_cookbook_gordon")
243 | expect(chef_run).not_to run_execute("upload_cookbook_emeril")
244 | end
245 | end
246 |
247 | context 'when user specifies they wish to upload to Chef Server' do
248 | before do
249 | allow(DeliveryTruck::Helpers::Publish).to receive(:upload_cookbook_to_chef_server?)
250 | .and_return(true)
251 | chef_run.node.default['delivery']['config']['delivery-truck']['publish']['chef_server'] = true
252 | end
253 |
254 | context 'and no cookbooks changed' do
255 | before do
256 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks)
257 | .and_return(no_changed_cookbooks)
258 | chef_run.converge(described_recipe)
259 | end
260 |
261 | it 'does nothing' do
262 | expect(chef_run).not_to create_link('/tmp/cache/cookbook-upload/julia')
263 | expect(chef_run).not_to create_link('/tmp/cache/cookbook-upload/gordon')
264 | expect(chef_run).not_to create_link('/tmp/cache/cookbook-upload/emeril')
265 |
266 | expect(chef_run).not_to run_execute("upload_cookbook_julia")
267 | expect(chef_run).not_to run_execute("upload_cookbook_gordon")
268 | expect(chef_run).not_to run_execute("upload_cookbook_emeril")
269 | end
270 | end
271 |
272 | context 'and one cookbook changed' do
273 | before do
274 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks)
275 | .and_return(one_changed_cookbook)
276 | chef_run.converge(described_recipe)
277 | end
278 |
279 | it 'uploads only that cookbook' do
280 | expect(chef_run).to create_link('/tmp/cache/cookbook-upload/julia')
281 | .with(to: '/tmp/repo/cookbooks/julia')
282 | expect(chef_run).not_to create_link('/tmp/cache/cookbook-upload/gordon')
283 | expect(chef_run).not_to create_link('/tmp/cache/cookbook-upload/emeril')
284 |
285 | expect(chef_run).to run_execute("upload_cookbook_julia")
286 | .with(command: 'knife cookbook upload julia ' \
287 | '--freeze --all --force ' \
288 | '--config /var/opt/delivery/workspace/.chef/knife.rb ' \
289 | '--cookbook-path /tmp/cache/cookbook-upload')
290 | expect(chef_run).not_to run_execute("upload_cookbook_gordon")
291 | expect(chef_run).not_to run_execute("upload_cookbook_emeril")
292 | end
293 | end
294 |
295 | context 'and multiple cookbooks changed' do
296 | before do
297 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks)
298 | .and_return(two_changed_cookbooks)
299 | chef_run.converge(described_recipe)
300 | end
301 |
302 | it 'uploads only those cookbook' do
303 | expect(chef_run).to create_link('/tmp/cache/cookbook-upload/julia')
304 | .with(to: '/tmp/repo/cookbooks/julia')
305 | expect(chef_run).to create_link('/tmp/cache/cookbook-upload/gordon')
306 | .with(to: '/tmp/repo/cookbooks/gordon')
307 | expect(chef_run).not_to create_link('/tmp/cache/cookbook-upload/emeril')
308 |
309 | expect(chef_run).to run_execute("upload_cookbook_julia")
310 | .with(command: 'knife cookbook upload julia ' \
311 | '--freeze --all --force ' \
312 | '--config /var/opt/delivery/workspace/.chef/knife.rb ' \
313 | '--cookbook-path /tmp/cache/cookbook-upload')
314 | expect(chef_run).to run_execute("upload_cookbook_gordon")
315 | .with(command: 'knife cookbook upload gordon ' \
316 | '--freeze --all --force ' \
317 | '--config /var/opt/delivery/workspace/.chef/knife.rb ' \
318 | '--cookbook-path /tmp/cache/cookbook-upload')
319 | expect(chef_run).not_to run_execute("upload_cookbook_emeril")
320 | end
321 | end
322 |
323 | context 'a Berksfile exists' do
324 | before do
325 | allow(File).to receive(:exist?).and_call_original
326 | allow(File).to receive(:exist?).with('/tmp/repo/cookbooks/julia/Berksfile')
327 | .and_return(true)
328 | allow_any_instance_of(Chef::Recipe).to receive(:changed_cookbooks)
329 | .and_return(one_changed_cookbook)
330 | chef_run.converge(described_recipe)
331 | end
332 |
333 | it 'vendors all dependencies with Berkshelf' do
334 |
335 | expect(chef_run).to run_execute("berks_vendor_cookbook_julia")
336 | .with(command: 'berks vendor /tmp/cache/cookbook-upload')
337 | .with(cwd: '/tmp/repo/cookbooks/julia')
338 |
339 | expect(chef_run).to run_execute("upload_cookbook_julia")
340 | .with(command: 'knife cookbook upload julia ' \
341 | '--freeze --all --force ' \
342 | '--config /var/opt/delivery/workspace/.chef/knife.rb ' \
343 | '--cookbook-path /tmp/cache/cookbook-upload')
344 | end
345 | end
346 | end
347 |
348 | context 'when they do not wish to push to github' do
349 | before do
350 | allow(DeliveryTruck::Helpers::Publish).to receive(:push_repo_to_github?)
351 | .and_return(false)
352 | stub_command("git remote --verbose | grep ^github").and_return(false)
353 | chef_run.converge(described_recipe)
354 | end
355 |
356 | it 'does not push to github' do
357 | expect(chef_run).not_to run_execute("push_to_github")
358 | end
359 | end
360 |
361 | context 'when they wish to push to github' do
362 | let(:secrets) {{'github' => 'SECRET'}}
363 |
364 | before do
365 | allow_any_instance_of(Chef::Recipe).to receive(:get_project_secrets)
366 | .and_return(secrets)
367 | stub_command("git remote --verbose | grep ^github").and_return(false)
368 | chef_run.node.default['delivery']['config']['delivery-truck']['publish']['github'] = 'spec/spec'
369 | chef_run.converge(described_recipe)
370 | end
371 |
372 | it 'pushes to github' do
373 | expect(chef_run).to push_delivery_github('spec/spec')
374 | .with(deploy_key: 'SECRET',
375 | branch: 'master',
376 | remote_url: 'git@github.com:spec/spec.git',
377 | repo_path: '/tmp/repo',
378 | cache_path: '/tmp/cache',
379 | action: [:push])
380 | end
381 | end
382 |
383 | context 'when they do not wish to push to git' do
384 | before do
385 | allow(DeliveryTruck::Helpers::Publish).to receive(:push_repo_to_git?)
386 | .and_return(false)
387 | chef_run.converge(described_recipe)
388 | end
389 |
390 | it 'does not push to git' do
391 | expect(chef_run).not_to run_execute("push_to_git")
392 | end
393 | end
394 |
395 | context 'when they wish to push to git' do
396 | let(:secrets) {{'git' => 'SECRET'}}
397 |
398 | before do
399 | allow_any_instance_of(Chef::Recipe).to receive(:get_project_secrets)
400 | .and_return(secrets)
401 | chef_run.node.default['delivery']['config']['delivery-truck']['publish']['git'] = 'ssh://git@stash:2222/spec/spec.git'
402 | chef_run.converge(described_recipe)
403 | end
404 |
405 | it 'pushes to git' do
406 | expect(chef_run).to push_delivery_github('ssh://git@stash:2222/spec/spec.git')
407 | .with(deploy_key: 'SECRET',
408 | branch: 'master',
409 | remote_url: 'ssh://git@stash:2222/spec/spec.git',
410 | repo_path: '/tmp/repo',
411 | cache_path: '/tmp/cache',
412 | action: [:push])
413 | end
414 | end
415 |
416 | end
417 |
--------------------------------------------------------------------------------
/libraries/helpers_provision.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2015 Chef Software, Inc.
3 | # License:: Apache License, Version 2.0
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | module DeliveryTruck
19 | module Helpers
20 | module Provision
21 | extend self
22 |
23 | def provision(stage_name, node, acceptance_env_name, cookbooks)
24 | if stage_name == 'acceptance'
25 | handle_acceptance_pinnings(node, acceptance_env_name, cookbooks)
26 | elsif stage_name == 'union'
27 | handle_union_pinnings(node, acceptance_env_name, cookbooks)
28 | elsif stage_name == 'rehearsal'
29 | handle_rehearsal_pinnings(node)
30 | elsif stage_name == 'delivered'
31 | handle_delivered_pinnings(node)
32 | else
33 | chef_log.info("Nothing to do for #{stage_name}, did you mean to copy this environment?")
34 | end
35 | end
36 |
37 | # Refresh Acceptance from Union without overwriting Acceptance's pins
38 | # for the current project's applications and cookbooks by:
39 | # 1) Pulling the current Acceptance version pins for apps and cookbooks
40 | # related to the project into memory before overwriting Acceptance.
41 | # 2) Overwrite Acceptance app and cookbook pinnings with Union to make sure
42 | # Acceptance environment is up to date with Union.
43 | # 3) Insert the preserved, original version pins on the apps and cookbooks
44 | # for the current project, resulting in an Acceptance that resembled
45 | # Union except for Acceptance's original pinnings for the current project.
46 | # 4) Copy over pins for all project cookbooks.
47 | def handle_acceptance_pinnings(node, acceptance_env_name, get_all_project_cookbooks)
48 | union_env_name = 'union'
49 |
50 | union_env = fetch_or_create_environment(union_env_name)
51 | acceptance_env = fetch_or_create_environment(acceptance_env_name)
52 |
53 | # Before we overwite acceptance with union,
54 | # remember the cookbook and application pinnings in acceptance for this project.
55 | cookbook_pinnings = project_cookbook_version_pins_from_env(node, acceptance_env)
56 | app_pinnings = project_application_version_pins_from_env(node, acceptance_env)
57 |
58 | ############################################################################
59 | # Copy Union State Onto Acceptance So Acceptance Looks Like Union To Start #
60 | ############################################################################
61 |
62 | # Pull and merge the pinnings and attrs from union into this acceptance env.
63 | chef_log.info("Pulling back environment from #{union_env_name} into #{acceptance_env_name}")
64 | promote_cookbook_versions(union_env, acceptance_env)
65 |
66 | if acceptance_env.override_attributes && !acceptance_env.override_attributes.empty? &&
67 | acceptance_env.override_attributes['applications'] != nil
68 | union_apps = union_env.override_attributes['applications']
69 | acceptance_env.override_attributes['applications'].merge!(union_apps) unless union_apps.nil?
70 | else
71 | acceptance_env.override_attributes(union_env.override_attributes)
72 | end
73 |
74 | ####################################################################
75 | # Overwrite Acceptance Pins For Project Related Cookbooks and Apps #
76 | ####################################################################
77 | cookbook_pinnings.each do |cb, pin|
78 | chef_log.info("Setting version pinning for #{cb} to what we" +
79 | " remembered earlier: (#{pin})")
80 | acceptance_env.cookbook(cb, pin)
81 | end
82 |
83 | # Make sure the outer key is there.
84 | if acceptance_env.override_attributes['applications'].nil?
85 | acceptance_env.override_attributes['applications'] = {}
86 | end
87 |
88 | app_pinnings.each do |app, version|
89 | chef_log.info("Setting version for app #{app} to what we" +
90 | " remembered earlier: (#{version})")
91 | acceptance_env.override_attributes['applications'][app] = version
92 | end
93 |
94 | # Copy over pins for any cookbook that changes.
95 | version_map = {}
96 |
97 | get_all_project_cookbooks.each do |cookbook|
98 | version_map[cookbook.name] = cookbook.version
99 | end
100 |
101 | version_map.each do |cookbook, version|
102 | acceptance_env.cookbook(cookbook, version)
103 | end
104 |
105 | acceptance_env.save
106 | acceptance_env
107 | end
108 |
109 | # Promote all cookbooks and apps related to the current project from
110 | # Acceptance to Union.
111 | def handle_union_pinnings(node, acceptance_env_name, project_cookbooks)
112 | union_env_name = 'union'
113 |
114 | acceptance_env = fetch_or_create_environment(acceptance_env_name)
115 | union_env = fetch_or_create_environment(union_env_name)
116 |
117 | union_env.default_attributes['delivery'] ||= {}
118 | union_env.default_attributes['delivery']['project_artifacts'] ||= {}
119 | union_env.default_attributes['delivery']['union_changes'] ||= []
120 |
121 | change_id = node['delivery']['change']['change_id']
122 |
123 | # There's a race condition where acceptance can be updated between re-runs of union
124 | # with changes that are note yet approved. Thus we don't want to re-promote pinnings
125 | # if we're in a re-run situation.
126 | unless union_env.default_attributes['delivery']['union_changes'].include?(change_id)
127 | union_env.default_attributes['delivery']['union_changes'] << change_id
128 | promote_project_cookbooks(node, acceptance_env, union_env, project_cookbooks)
129 | promote_project_apps(node, acceptance_env, union_env)
130 |
131 | ## Update cached project metadata
132 | project_name = project_name(node)
133 | union_env.default_attributes['delivery']['project_artifacts'][project_name] ||= {}
134 | populate_project_artifacts(node, project_cookbooks, acceptance_env, union_env)
135 | union_env.save
136 | end
137 |
138 | union_env
139 | end
140 |
141 | def handle_rehearsal_pinnings(node)
142 | union_env = fetch_or_create_environment('union')
143 | cleanup_union_changes(union_env, node)
144 |
145 | blocked = ::DeliveryTruck::DeliveryApiClient.blocked_projects(node)
146 |
147 | rehearsal_env = fetch_or_create_environment('rehearsal')
148 |
149 | chef_log.info("current environment: #{rehearsal_env.name}")
150 | chef_log.info("promoting pinnings from environment: #{union_env.name}")
151 |
152 | promote_unblocked_cookbooks_and_applications(union_env, rehearsal_env, blocked)
153 |
154 | chef_log.info("Promoting environment from #{union_env.name} to #{rehearsal_env.name}")
155 |
156 | rehearsal_env.save
157 | rehearsal_env
158 | end
159 |
160 | # This introduces a race condition with a small target window. If
161 | # union/provision and rehearsal/provision end up running simultaneously
162 | # the union env could end up in an unknown state because we are doing a
163 | # read/modify/write in both places.
164 | def cleanup_union_changes(union_env, node)
165 | union_changes = union_env.default_attributes['delivery']['union_changes'] || []
166 | union_changes.delete(node['delivery']['change']['change_id'])
167 | union_env.default_attributes['delivery']['union_changes'] = union_changes
168 | union_env.save
169 | end
170 |
171 | # Promote the from_env's attributes and cookbook_verions to to_env.
172 | # We want rehearsal to match union, and delivered to match rehearsal
173 | # so we promote all cookbook_versions, default_attributes, and
174 | # override_attributes (not just for the current project, but everything
175 | # in from_env).
176 | def handle_delivered_pinnings(node)
177 | to_env_name = 'delivered'
178 | from_env_name = 'rehearsal'
179 |
180 | chef_log.info("current environment: #{to_env_name}")
181 | chef_log.info("promoting pinnings from environment: #{from_env_name}")
182 |
183 | from_env = fetch_or_create_environment(from_env_name)
184 | to_env = fetch_or_create_environment(to_env_name)
185 |
186 | promote_cookbook_versions(from_env, to_env)
187 | promote_default_attributes(from_env, to_env)
188 | promote_override_attributes(from_env, to_env)
189 |
190 | chef_log.info("Promoting environment from #{from_env_name} to #{to_env_name}")
191 |
192 | # TODO: protect against this?
193 | # From here on out, we have broken the environment unless all the cookbook
194 | # constraints get satisfied. Heads way the hell up, kids.
195 | to_env.save
196 | to_env
197 | end
198 |
199 | #################################
200 | # Helper methods
201 | #################################
202 |
203 | def chef_log
204 | Chef::Log
205 | end
206 |
207 | def project_name(node)
208 | node['delivery']['change']['project']
209 | end
210 |
211 | def fetch_or_create_environment(env_name)
212 | env = Chef::Environment.load(env_name)
213 | rescue Net::HTTPServerException => http_e
214 | raise http_e unless http_e.response.code == "404"
215 | chef_log.info("Creating Environment #{env_name}")
216 | env = Chef::Environment.new()
217 | env.name(env_name)
218 | env.create
219 | end
220 |
221 | # Sets the node.default value ['delivery']['project_cookbooks'] based on
222 | # the node's current value for ['delivery']['project_cookbooks'], using
223 | # the project name as a default if project_cookbooks not set.
224 | def set_project_cookbooks(node)
225 | default_cookbooks = [project_name(node)]
226 | unless node['delivery']['project_cookbooks']
227 | node.default['delivery']['project_cookbooks'] = default_cookbooks
228 | end
229 | end
230 |
231 | # Sets the node.default value ['delivery']['project_apps'] based on
232 | # the node's current value for ['delivery']['project_apps'], using
233 | # the project name as a default if project_cookbooks not set.
234 | def set_project_apps(node)
235 | default_apps = [project_name(node)]
236 | unless node['delivery']['project_apps']
237 | node.default['delivery']['project_apps'] = default_apps
238 | end
239 | end
240 |
241 | # Determines which cookbooks and applications are a part of this project and
242 | # updates union_env's project_artifacts accordingly
243 | # Does _not_ call save on the enviornment so that changes can be more transactional
244 | def populate_project_artifacts(node, project_cookbooks, acceptance_env, union_env)
245 | # Can't blindly set project_artifacts based on project_apps and project_cookbooks,
246 | # must check if anything actually exists on the acceptance env
247 | # like the rest of the code does.
248 | new_applications = []
249 | if acceptance_env.override_attributes['applications']
250 | node['delivery']['project_apps'].each do |app|
251 | new_applications << app if acceptance_env.override_attributes['applications'][app]
252 | end
253 | end
254 | union_env.default_attributes['delivery']['project_artifacts'][project_name(node)]['applications'] = new_applications
255 |
256 | new_cookbooks = []
257 | node['delivery']['project_cookbooks'].each do |cookbook|
258 | if acceptance_env.cookbook_versions[cookbook]
259 | new_cookbooks << cookbook
260 | end
261 | end
262 |
263 | # project_cookbooks is something that's set by the user's build cookbook.
264 | # We're also pulling in any cookbooks we auto-detect for backwards compatability
265 | project_cookbooks.each do |cookbook|
266 | new_cookbooks << cookbook.name unless new_cookbooks.include?(cookbook.name)
267 | end
268 |
269 | union_env.default_attributes['delivery']['project_artifacts'][project_name(node)]['cookbooks'] = new_cookbooks
270 | end
271 |
272 | # Returns a hash of {cookbook_name => pin, ...} where pin is the passed
273 | # environment's pin for all project_cookbooks from the node.
274 | # Cookbooks that do no have an environment pin are excluded.
275 | def project_cookbook_version_pins_from_env(node, env)
276 | pinnings = {}
277 | set_project_cookbooks(node)
278 |
279 | chef_log.info("Checking #{env.name} pinnings for" +
280 | " #{node['delivery']['project_cookbooks']}")
281 | node['delivery']['project_cookbooks'].each do |pin|
282 | if env.cookbook_versions[pin]
283 | pinnings[pin] = env.cookbook_versions[pin]
284 | end
285 | end
286 |
287 | chef_log.info("Remembering pinning for #{pinnings}...")
288 | pinnings
289 | end
290 |
291 | # Returns a hash of {application_name => pin, ...} where pin is the passed
292 | # environment's override_attributes pin for all project_apps from the node.
293 | # Apps that do not have an environment pin are excluded.
294 | def project_application_version_pins_from_env(node, env)
295 | pinnings = {}
296 | set_project_apps(node)
297 |
298 | chef_log.info("Checking #{env.name} apps for" +
299 | " #{node['delivery']['project_apps']}")
300 | node['delivery']['project_apps'].each do |app|
301 | if env.override_attributes['applications'] &&
302 | env.override_attributes['applications'][app]
303 | pinnings[app] = env.override_attributes['applications'][app]
304 | end
305 | end
306 |
307 | chef_log.info("Remembering app versions for #{pinnings}...")
308 | pinnings
309 | end
310 |
311 | # Set promoted_on_env's cookbook_verions pins to promoted_from_env's
312 | # cookbook_verions pins for all project_cookbooks.
313 | # This promotes all cookbooks related to the project in promoted_from_env to promoted_on_env.
314 | def promote_project_cookbooks(node, promoted_from_env, promoted_on_env, project_cookbooks)
315 | set_project_cookbooks(node)
316 |
317 | all_project_cookbooks = []
318 | project_cookbooks.each do |cookbook|
319 | all_project_cookbooks << cookbook.name
320 | end
321 |
322 | all_project_cookbooks.concat(node['delivery']['project_cookbooks'])
323 |
324 | all_project_cookbooks.each do |pin|
325 | from_v = promoted_from_env.cookbook_versions[pin]
326 | to_v = promoted_on_env.cookbook_versions[pin]
327 | if from_v
328 | chef_log.info("Promoting #{pin} @ #{from_v} from #{promoted_from_env.name}" +
329 | " to #{promoted_on_env.name} was @ #{to_v}.")
330 | promoted_on_env.cookbook_versions[pin] = from_v
331 | end
332 | end
333 | end
334 |
335 | # Set promoted_on_env's application pins to promoted_from_env's
336 | # application pins for all project_apps (or the base project
337 | # if no project_apps set). This promotes all applications in
338 | # promoted_from_env to promoted_on_env.
339 | def promote_project_apps(node, promoted_from_env, promoted_on_env)
340 | set_project_apps(node)
341 |
342 | ## Make sure the outer key is there
343 | if promoted_on_env.override_attributes['applications'].nil?
344 | promoted_on_env.override_attributes['applications'] = {}
345 | end
346 |
347 | node['delivery']['project_apps'].each do |app|
348 | from_v = promoted_from_env.override_attributes['applications'][app]
349 | to_v = promoted_on_env.override_attributes['applications'][app]
350 | if from_v
351 | chef_log.info("Promoting #{app} @ #{from_v} from #{promoted_from_env.name}" +
352 | " to #{promoted_on_env.name} was @ #{to_v}.")
353 | promoted_on_env.override_attributes['applications'][app] = from_v
354 | end
355 | end
356 | end
357 |
358 | # Simply set promoted_on_env's cookbook_versions to match
359 | # promoted_from_env's cookbook_verions for every cookbook that exists in
360 | # the latter.
361 | def promote_cookbook_versions(promoted_from_env, promoted_on_env)
362 | promoted_on_env.cookbook_versions(promoted_from_env.cookbook_versions)
363 | end
364 |
365 | def promote_unblocked_cookbooks_and_applications(promoted_from_env, promoted_on_env, blocked)
366 | if blocked.empty?
367 | promote_cookbook_versions(promoted_from_env, promoted_on_env)
368 | promote_default_attributes(promoted_from_env, promoted_on_env)
369 | promote_override_attributes(promoted_from_env, promoted_on_env)
370 | return
371 | end
372 | # Initialize the attributes if they don't exist.
373 | promoted_on_env.default_attributes['delivery'] ||= {}
374 | promoted_on_env.default_attributes['delivery']['project_artifacts'] ||= {}
375 | promoted_on_env.override_attributes['applications'] ||= {}
376 |
377 | promoted_from_env.default_attributes['delivery']['project_artifacts'].each do |project_name, project_contents|
378 | if blocked.include?(project_name)
379 | chef_log.info("Project #{project_name} is currently blocked." +
380 | "not promoting its cookbooks or applications")
381 | else
382 | chef_log.info("Promoting cookbooks and applications for project #{project_name}")
383 |
384 | # promote cookbooks
385 | (project_contents['cookbooks'] || []).each do |cookbook_name|
386 | if promoted_from_env.cookbook_versions[cookbook_name]
387 | promoted_on_env.cookbook_versions[cookbook_name] = promoted_from_env.cookbook_versions[cookbook_name]
388 | else
389 | chef_log.warn("Unable to promote cookbook '#{cookbook_name}' because " +
390 | "it does not exist in #{promoted_from_env.name} environment.")
391 | end
392 | end
393 |
394 | promoted_on_env.default_attributes['delivery']['project_artifacts'][project_name] = project_contents
395 |
396 | (project_contents['applications'] || []).each do |app_name|
397 | if promoted_from_env.override_attributes['applications'][app_name]
398 | promoted_on_env.override_attributes['applications'][app_name] =
399 | promoted_from_env.override_attributes['applications'][app_name]
400 | else
401 | chef_log.warn("Unable to promote application '#{app_name}' because " +
402 | "it does not exist in #{promoted_from_env.name} environment.")
403 | end
404 | end
405 | end
406 | end
407 | end
408 |
409 | # Simply set promoted_on_env's default_attributes to match
410 | # promoted_from_env's
411 | def promote_default_attributes(promoted_from_env, promoted_on_env)
412 | if promoted_on_env.default_attributes && !promoted_on_env.default_attributes.empty?
413 | promoted_on_env.default_attributes.merge!(promoted_from_env.default_attributes)
414 | else
415 | promoted_on_env.default_attributes(promoted_from_env.default_attributes)
416 | end
417 | end
418 |
419 | # Simply set promoted_on_env's override_attributes to match
420 | # promoted_from_env's override_attributes for every cookbook that exists in
421 | # the latter.
422 | def promote_override_attributes(promoted_from_env, promoted_on_env)
423 | ## Only a one-level deep hash merge
424 | if promoted_on_env.override_attributes && !promoted_on_env.override_attributes.empty?
425 | promoted_on_env.override_attributes.merge!(promoted_from_env.override_attributes)
426 | else
427 | promoted_on_env.override_attributes(promoted_from_env.override_attributes)
428 | end
429 | end
430 | end
431 | end
432 | end
433 |
--------------------------------------------------------------------------------
/spec/unit/libraries/helpers_provision_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DeliveryTruck::Helpers::Provision do
4 | let(:project_name) { 'delivery' }
5 | let(:change_id) { 'change-id' }
6 |
7 | let(:node) { instance_double('Chef::Node') }
8 |
9 | let(:node) do
10 | node = Chef::Node.new
11 | node.default_attrs = node_attributes
12 | node
13 | end
14 |
15 | let(:node_attributes) do
16 | {
17 | 'delivery' => {
18 | 'change' => {
19 | 'project' => project_name,
20 | 'change_id' => change_id
21 | }
22 | }
23 | }
24 | end
25 |
26 | describe '#provision' do
27 | let(:acceptance_env_name) { 'acceptance' }
28 | let(:cookbooks) { [] }
29 |
30 | context 'acceptance' do
31 | let(:stage_name) { 'acceptance' }
32 |
33 | it 'handles accepance' do
34 | expect(subject).to receive(:handle_acceptance_pinnings).with(node, acceptance_env_name, cookbooks)
35 |
36 | subject.provision stage_name, node, acceptance_env_name, cookbooks
37 | end
38 | end
39 |
40 | context 'union' do
41 | let(:stage_name) { 'union' }
42 |
43 | it 'handles union' do
44 | expect(subject).to receive(:handle_union_pinnings).with(node, acceptance_env_name, cookbooks)
45 |
46 | subject.provision stage_name, node, acceptance_env_name, cookbooks
47 | end
48 | end
49 |
50 | context 'rehearsal' do
51 | let(:stage_name) { 'rehearsal' }
52 |
53 | it 'handles union' do
54 | expect(subject).to receive(:handle_rehearsal_pinnings).with(node)
55 |
56 | subject.provision stage_name, node, acceptance_env_name, cookbooks
57 | end
58 | end
59 |
60 | context 'delivered' do
61 | let(:stage_name) { 'delivered' }
62 |
63 | it 'handles union' do
64 | expect(subject).to receive(:handle_delivered_pinnings).with(node)
65 |
66 | subject.provision stage_name, node, acceptance_env_name, cookbooks
67 | end
68 | end
69 |
70 | context 'unknown' do
71 | let(:stage_name) { 'unknown' }
72 |
73 | it 'logs' do
74 | expect(::Chef::Log).to receive(:info).with("Nothing to do for #{stage_name}, did you mean to copy this environment?")
75 |
76 | subject.provision stage_name, node, acceptance_env_name, cookbooks
77 | end
78 |
79 | end
80 | end
81 |
82 | describe '.project_cookbook_version_pins_from_env' do
83 | let(:cookbook_versions) do
84 | {
85 | project_name => '= 0.3.0',
86 | 'cookbook-1' => '= 1.2.0',
87 | 'cookbook-2' => '= 2.0.0'
88 | }
89 | end
90 |
91 | let(:env) do
92 | env = Chef::Environment.new
93 | env.name('test-env')
94 | env.cookbook_versions(cookbook_versions)
95 | env
96 | end
97 |
98 | context 'when the project is a cookbook' do
99 | it 'returns the cookbook version pinning' do
100 | expected_cookbook_pinnings = {
101 | project_name => '= 0.3.0'
102 | }
103 | cookbook_pinnings =
104 | described_class.project_cookbook_version_pins_from_env(node, env)
105 | expect(cookbook_pinnings).to eq(expected_cookbook_pinnings)
106 | end
107 | end
108 |
109 | describe 'when the repository contains multiple project cookbooks' do
110 | before(:each) do
111 | node_attributes['delivery']['project_cookbooks'] = ['cookbook-1',
112 | 'cookbook-2']
113 | end
114 |
115 | it 'returns the version pinnings of the project cookbooks' do
116 | expected_cookbook_pinnings = {
117 | 'cookbook-1' => '= 1.2.0',
118 | 'cookbook-2' => '= 2.0.0'
119 | }
120 | cookbook_pinnings =
121 | described_class.project_cookbook_version_pins_from_env(node, env)
122 | expect(cookbook_pinnings).to eq(expected_cookbook_pinnings)
123 | end
124 | end
125 | end
126 |
127 | describe '.project_application_version_pins_from_env' do
128 | let(:env) do
129 | env = Chef::Environment.new
130 | env.name('test-env')
131 | env.override_attributes = {
132 | 'applications' => application_versions
133 | }
134 | env
135 | end
136 |
137 | context 'when the project is not an application' do
138 | let(:application_versions) { {} }
139 |
140 | it 'returns no application version pinnings' do
141 | expect(
142 | described_class.project_application_version_pins_from_env(node, env)
143 | ).to eq({})
144 | end
145 |
146 | context 'when the project is an application' do
147 | let(:application_versions) do
148 | {
149 | project_name => '0_3_562'
150 | }
151 | end
152 |
153 | it 'sets the project as the project application and returns project' \
154 | ' application version pinning' do
155 | expect(
156 | described_class.project_application_version_pins_from_env(node, env)
157 | ).to eq({project_name => '0_3_562'})
158 | end
159 | end
160 | end
161 |
162 | context 'when the project contains multiple applications' do
163 | let(:application_versions) do
164 | {
165 | 'app-1' => '0_3_562',
166 | 'app-2' => '5_0_102'
167 | }
168 | end
169 |
170 | before(:each) do
171 | node_attributes['delivery']['project_apps'] = [ 'app-1', 'app-2' ]
172 | end
173 |
174 | it 'sets the applications as the project applications and returns' \
175 | ' project application version pinnings' do
176 | expect(
177 | described_class.project_application_version_pins_from_env(node, env)
178 | ).to eq({
179 | 'app-1' => '0_3_562',
180 | 'app-2' => '5_0_102'
181 | })
182 | end
183 | end
184 | end
185 |
186 | describe '.handle_acceptance_pinnings' do
187 | let(:acceptance_env_name) { 'acceptance-chef-cookbooks-delivery-truck' }
188 |
189 | let(:project_version) { '= 0.1.2' }
190 | let(:project_version_in_union) { '= 0.1.1' }
191 |
192 | let(:acceptance_env) do
193 | env = Chef::Environment.new()
194 | env.name(acceptance_env_name)
195 | env.cookbook_versions(acceptance_cookbook_versions)
196 | env.override_attributes = {
197 | 'applications' => acceptance_application_versions
198 | }
199 | env
200 | end
201 |
202 | let(:union_env) do
203 | env = Chef::Environment.new()
204 | env.name('union')
205 | env.cookbook_versions(union_cookbook_versions)
206 | env.override_attributes = {
207 | 'applications' => union_application_versions
208 | }
209 | env
210 | end
211 |
212 | before do
213 | expect(described_class).
214 | to receive(:fetch_or_create_environment).
215 | with(acceptance_env_name).
216 | and_return(acceptance_env)
217 | expect(described_class).
218 | to receive(:fetch_or_create_environment).
219 | with('union').
220 | and_return(union_env)
221 | expect(acceptance_env).to receive(:save)
222 | end
223 |
224 | context 'when the project is a cookbook' do
225 | let(:acceptance_cookbook_versions) do
226 | {
227 | project_name => project_version
228 | }
229 | end
230 |
231 | let(:acceptance_application_versions) { {} }
232 |
233 | let(:union_cookbook_versions) do
234 | {
235 | project_name => project_version_in_union,
236 | 'cookbook-1' => '= 1.2.0',
237 | 'cookbook-2' => '= 2.0.0'
238 | }
239 | end
240 |
241 | let(:union_application_versions) do
242 | {
243 | 'delivery-app' => '0_3_562'
244 | }
245 | end
246 |
247 | let(:cookbook) { instance_double('DeliverySugar::Cookbook') }
248 |
249 | let(:get_all_project_cookbooks) do
250 | [cookbook]
251 | end
252 |
253 | before do
254 | allow(cookbook).to receive(:name).and_return(project_name)
255 | allow(cookbook).to receive(:version).and_return(project_version)
256 | end
257 |
258 | it 'copies cookbook and application version pinnings from the union' \
259 | ' environment to the acceptance environment and updates the cookbook' \
260 | ' version pinning in the acceptance environment' do
261 | expected_cookbook_versions = {
262 | project_name => project_version,
263 | 'cookbook-1' => '= 1.2.0',
264 | 'cookbook-2' => '= 2.0.0'
265 | }
266 | expected_application_versions = {
267 | 'delivery-app' => '0_3_562'
268 | }
269 | acceptance_env_result =
270 | described_class.handle_acceptance_pinnings(node, acceptance_env_name, get_all_project_cookbooks)
271 | expect(acceptance_env_result.cookbook_versions).
272 | to eq(expected_cookbook_versions)
273 | expect(acceptance_env_result.override_attributes['applications']).
274 | to eq(expected_application_versions)
275 | end
276 | end
277 |
278 | context 'when the project is an application' do
279 | let(:acceptance_cookbook_versions) { {} }
280 |
281 | let(:acceptance_application_versions) do
282 | {
283 | project_name => project_version
284 | }
285 | end
286 |
287 | let(:union_cookbook_versions) do
288 | {
289 | 'cookbook-1' => '= 1.2.0',
290 | 'cookbook-2' => '= 2.0.0'
291 | }
292 | end
293 |
294 | let(:union_application_versions) do
295 | {
296 | project_name => project_version_in_union,
297 | 'delivery-app' => '0_3_562'
298 | }
299 | end
300 |
301 | before(:each) do
302 | node.default['delivery']['project_cookbooks'] = []
303 | end
304 |
305 | let(:get_all_project_cookbooks) do
306 | []
307 | end
308 |
309 | it 'copies the cookbook and application version pinnings from the union' \
310 | ' environment to the acceptance environment and updates the application' \
311 | ' version pinning in the acceptance environment' do
312 | expected_cookbook_versions = {
313 | 'cookbook-1' => '= 1.2.0',
314 | 'cookbook-2' => '= 2.0.0'
315 | }
316 | expected_application_versions = {
317 | project_name => project_version,
318 | 'delivery-app' => '0_3_562'
319 | }
320 | acceptance_env_result =
321 | described_class.handle_acceptance_pinnings(node, acceptance_env_name, get_all_project_cookbooks)
322 | expect(acceptance_env_result.cookbook_versions).
323 | to eq(expected_cookbook_versions)
324 | expect(acceptance_env_result.override_attributes['applications']).
325 | to eq(expected_application_versions)
326 | end
327 | end
328 |
329 | context 'a project with cookbooks and applications' do
330 | let(:project_cookbook_name) { 'delivery-cookbook' }
331 | let(:project_cookbook_version) { '= 0.3.2' }
332 | let(:project_cookbook_version_in_union) { '= 0.3.0' }
333 |
334 | let(:project_app_name) { 'delivery-app' }
335 | let(:project_app_version) { '0_3_562' }
336 | let(:project_app_version_in_union) { '0_3_561' }
337 |
338 | let(:acceptance_cookbook_versions) do
339 | {
340 | project_cookbook_name => project_cookbook_version
341 | }
342 | end
343 |
344 | let(:acceptance_application_versions) do
345 | {
346 | project_app_name => project_app_version
347 | }
348 | end
349 |
350 | let(:union_cookbook_versions) do
351 | {
352 | project_cookbook_name => project_cookbook_version_in_union,
353 | 'cookbook-1' => '= 1.2.0',
354 | 'cookbook-2' => '= 2.0.0'
355 | }
356 | end
357 |
358 | let(:union_application_versions) do
359 | {
360 | project_app_name => project_app_version_in_union,
361 | 'delivery-app' => '0_3_562'
362 | }
363 | end
364 |
365 | let(:cookbook) { instance_double('DeliverySugar::Cookbook') }
366 |
367 | let(:get_all_project_cookbooks) do
368 | [cookbook]
369 | end
370 |
371 | before(:each) do
372 | node.default['delivery']['project_cookbooks'] = [project_cookbook_name]
373 | node.default['delivery']['project_apps'] = [project_app_name]
374 | allow(cookbook).to receive(:name).and_return(project_cookbook_name)
375 | allow(cookbook).to receive(:version).and_return(project_cookbook_version)
376 | end
377 |
378 | it 'copies the cookbook and application version pinnings from the union' \
379 | ' environment to the acceptance environment and updates the cookbook' \
380 | ' and application version pinnings in the acceptance environment' do
381 | expected_cookbook_versions = {
382 | project_cookbook_name => project_cookbook_version,
383 | 'cookbook-1' => '= 1.2.0',
384 | 'cookbook-2' => '= 2.0.0'
385 | }
386 | expected_application_versions = {
387 | project_app_name => project_app_version,
388 | 'delivery-app' => '0_3_562'
389 | }
390 | acceptance_env_result =
391 | described_class.handle_acceptance_pinnings(node, acceptance_env_name, get_all_project_cookbooks)
392 | expect(acceptance_env_result.cookbook_versions).
393 | to eq(expected_cookbook_versions)
394 | expect(acceptance_env_result.override_attributes['applications']).
395 | to eq(expected_application_versions)
396 | end
397 | end
398 | end
399 |
400 | describe '.handle_union_pinnings' do
401 | let(:acceptance_env_name) { 'acceptance-chef-cookbooks-delivery-truck' }
402 |
403 | let(:project_version) { '= 0.1.2' }
404 | let(:project_version_in_acceptance) { '= 0.1.1' }
405 |
406 | let(:acceptance_env) do
407 | env = Chef::Environment.new()
408 | env.name(acceptance_env_name)
409 | env.cookbook_versions(acceptance_cookbook_versions)
410 | env.override_attributes = {
411 | 'applications' => acceptance_application_versions
412 | }
413 | env
414 | end
415 |
416 | let(:union_env) do
417 | env = Chef::Environment.new()
418 | env.name('union')
419 | env.cookbook_versions(union_cookbook_versions)
420 | env.override_attributes = {
421 | 'applications' => union_application_versions
422 | }
423 | env
424 | end
425 |
426 | before(:each) do
427 | expect(Chef::Environment).
428 | to receive(:load).
429 | with(acceptance_env_name).
430 | and_return(acceptance_env)
431 | expect(Chef::Environment).
432 | to receive(:load).
433 | with('union').
434 | and_return(union_env)
435 | expect(union_env).
436 | to receive(:save)
437 | end
438 |
439 | let(:passed_in_project_cookbooks) { [] }
440 |
441 | context 'when the project is a cookbook' do
442 | let(:acceptance_application_versions) { {} }
443 |
444 | let(:acceptance_cookbook_versions) do
445 | {
446 | project_name => project_version_in_acceptance }
447 | end
448 |
449 | let(:union_application_versions) do
450 | {
451 | 'an_application' => '= 3.2.0',
452 | 'another_application' => '= 2.2.4'
453 | }
454 | end
455 |
456 | let(:union_cookbook_versions) do
457 | {
458 | project_name => project_version,
459 | 'an_cookbook' => '= 0.3.1',
460 | 'another_cookbook' => '= 2.0.0'
461 | }
462 | end
463 |
464 | context 'when project cookbooks are detected' do
465 | let(:project_cookbook_name) { "changed_cookbook_that_is_not_in_project_cookbook_attributes" }
466 | let(:project_cookbook_version) { "= 0.1.0" }
467 |
468 | let(:acceptance_cookbook_versions) do
469 | {
470 | project_name => project_version_in_acceptance,
471 | project_cookbook_name => project_cookbook_version
472 | }
473 | end
474 |
475 | let(:project_cookbook) { instance_double('DeliverySugar::Cookbook') }
476 |
477 | before do
478 | allow(project_cookbook).to receive(:name).and_return(project_cookbook_name)
479 | allow(project_cookbook).to receive(:version).and_return(project_cookbook_version)
480 | end
481 |
482 | it 'copies cookbook version pinnings from the acceptance environment' \
483 | ' to the union environment' do
484 | expected_union_cookbook_versions =
485 | union_cookbook_versions.dup # copy, don't mutate incoming test state
486 | expected_union_cookbook_versions[project_name] =
487 | project_version_in_acceptance
488 | expected_union_cookbook_versions[project_cookbook_name] =
489 | project_cookbook_version
490 |
491 | union_env_result =
492 | described_class.handle_union_pinnings(node, acceptance_env_name, [project_cookbook])
493 |
494 | expect(union_env_result.cookbook_versions).
495 | to eq(expected_union_cookbook_versions)
496 | expect(union_env_result.override_attributes['applications']).
497 | to eq(union_application_versions)
498 | end
499 |
500 | it 'does not update pinnings if change id has already been updated' do
501 | first_union_env_result =
502 | described_class.handle_union_pinnings(node, acceptance_env_name, [project_cookbook])
503 |
504 | modified_acceptance_env = Chef::Environment.new()
505 | modified_acceptance_env.name(acceptance_env_name)
506 | modified_acceptance_env.cookbook_versions(
507 | acceptance_cookbook_versions.merge(new_cookbook: '1.1.1'))
508 |
509 | expect(described_class).
510 | to receive(:fetch_or_create_environment).
511 | with(acceptance_env_name).
512 | and_return(modified_acceptance_env)
513 | expect(described_class).
514 | to receive(:fetch_or_create_environment).
515 | with('union').
516 | and_return(first_union_env_result)
517 |
518 | seccond_union_env_result =
519 | described_class.handle_union_pinnings(node, acceptance_env_name, [project_cookbook])
520 | expect(first_union_env_result).to eq(seccond_union_env_result)
521 | end
522 | end
523 |
524 | describe 'cached project metadata' do
525 | context 'when no cached project metadata exists' do
526 | # This case will happen once when the build cookbook is upgraded
527 | # to pull in a version of delivery-truck which has this feature
528 | it 'caches the project metadata' do
529 | expected_project_metadata = {
530 | project_name => {
531 | 'cookbooks' => [project_name],
532 | # You only populate if acceptance_env.override_attributes['applications']
533 | # actually contains an application named `project_name`, which is
534 | # not the case in this test.
535 | 'applications' => []
536 | }
537 | }
538 |
539 | union_env_result =
540 | described_class.handle_union_pinnings(node, acceptance_env_name, passed_in_project_cookbooks)
541 |
542 | expect(union_env_result.default_attributes['delivery']['project_artifacts']).
543 | to eq(expected_project_metadata)
544 | end
545 | end
546 |
547 | context 'when the project is new' do
548 | let(:projects_metadata) do
549 | {
550 | 'project-foo' => {
551 | 'cookbooks' => [],
552 | 'applications' => ['project-foo-app']
553 | },
554 | 'project-bar' => {
555 | 'cookbooks' => ['project-bar-1', 'project-bar-1'],
556 | 'applications' => ['project-bar-app']
557 | }
558 | }
559 | end
560 |
561 | before(:each) do
562 | union_env.default_attributes = {
563 | 'delivery' => { 'project_artifacts' => projects_metadata }
564 | }
565 | end
566 |
567 | it 'adds the project cookbook to the cached projects metadata' do
568 | expected_projects_metadata = projects_metadata.dup
569 | expected_projects_metadata[project_name] = {
570 | 'cookbooks' => [project_name],
571 | 'applications' => []
572 | }
573 |
574 | union_env_result =
575 | described_class.handle_union_pinnings(node, acceptance_env_name, passed_in_project_cookbooks)
576 |
577 | expect(union_env_result.default_attributes['delivery']['project_artifacts']).
578 | to eq(expected_projects_metadata)
579 | end
580 | end
581 |
582 | context 'when the project metadata changes' do
583 | let(:projects_metadata) do
584 | {
585 | project_name => {
586 | 'cookbooks' => ["#{project_name}-1", "#{project_name}-2"],
587 | 'applications' => []
588 | }
589 | }
590 | end
591 |
592 | before(:each) do
593 | union_env.default_attributes = {
594 | 'delivery' => { 'project_artifacts' => projects_metadata }
595 | }
596 | end
597 |
598 | it 'updates the project metadata in the cache' do
599 | expected_projects_metadata = projects_metadata.dup
600 | expected_projects_metadata[project_name] = {
601 | 'cookbooks' => [project_name],
602 | 'applications' => []
603 | }
604 |
605 | union_env_result =
606 | described_class.handle_union_pinnings(node, acceptance_env_name, passed_in_project_cookbooks)
607 |
608 | expect(union_env_result.default_attributes['delivery']['project_artifacts']).
609 | to eq(expected_projects_metadata)
610 | end
611 | end
612 | end
613 | end
614 |
615 | context 'when the project is an application' do
616 | let(:acceptance_application_versions) do
617 | {
618 | project_name => project_version_in_acceptance
619 | }
620 | end
621 |
622 | let(:acceptance_cookbook_versions) do
623 | {
624 | project_name => project_version
625 | }
626 | end
627 |
628 | let(:union_application_versions) do
629 | {
630 | project_name => project_version,
631 | 'an_application' => '= 3.2.0',
632 | 'another_application' => '= 2.2.4'
633 | }
634 | end
635 |
636 | let(:union_cookbook_versions) do
637 | {
638 | 'an_cookbook' => '= 0.3.1',
639 | 'another_cookbook' => '= 2.0.0'
640 | }
641 | end
642 |
643 | before(:each) do
644 | node.default['delivery']['project_cookbooks'] = nil
645 | end
646 |
647 | context "cached project metadata" do
648 | let(:acceptance_env) do
649 | env = Chef::Environment.new()
650 | env.name(acceptance_env_name)
651 | env.cookbook_versions(acceptance_cookbook_versions)
652 | env.override_attributes = {
653 | 'applications' => {
654 | 'app1' => '= 1.0.0',
655 | 'app2' => '= 1.0.0',
656 | 'app3' => '= 1.0.0'
657 | }
658 | }
659 | env
660 | end
661 |
662 | it "saved all app names for the current project that have valid values" \
663 | "in the acceptance env" do
664 | app_names = ["app1", "app2", "app3"]
665 | node.default['delivery']['project_apps'] = app_names
666 |
667 | expected_project_metadata = {
668 | project_name => {
669 | 'cookbooks' => [project_name],
670 | 'applications' => app_names
671 | }
672 | }
673 |
674 | union_env_result =
675 | described_class.handle_union_pinnings(node, acceptance_env_name, passed_in_project_cookbooks)
676 |
677 | expect(union_env_result.default_attributes['delivery']['project_artifacts']).
678 | to eq(expected_project_metadata)
679 | end
680 | end
681 |
682 | it 'copies application version pinnings from the acceptance environment' \
683 | ' to the union environment' do
684 | expected_union_application_versions = union_application_versions.dup
685 | expected_union_application_versions[project_name] =
686 | project_version_in_acceptance
687 |
688 | union_env_result =
689 | described_class.handle_union_pinnings(node, acceptance_env_name, passed_in_project_cookbooks)
690 |
691 | expect(node['delivery']['project_apps']).to eq([project_name])
692 | expect(union_env_result.cookbook_versions).
693 | to eq(union_cookbook_versions)
694 | expect(union_env_result.override_attributes['applications']).
695 | to eq(expected_union_application_versions)
696 | end
697 | end
698 |
699 | context 'a project with applications and cookbooks' do
700 | let(:project_app_name) { 'delivery-app' }
701 | let(:project_app_version) { '0_3_562' }
702 | let(:project_app_version_in_acceptance) { '0_3_563' }
703 |
704 | let(:project_cookbook_names) { ['delivery-cookbook-1', 'delivery-cookbook-2'] }
705 | let(:project_cookbook_versions) { ['= 0.3.0', '= 1.0.2'] }
706 | let(:project_cookbook_versions_in_acceptance) { ['= 0.3.2', '= 1.0.4'] }
707 |
708 | let(:acceptance_application_versions) do
709 | {
710 | project_app_name => project_app_version_in_acceptance
711 | }
712 | end
713 |
714 | let(:acceptance_cookbook_versions) do
715 | {
716 | project_cookbook_names[0] => project_cookbook_versions_in_acceptance[0],
717 | project_cookbook_names[1] => project_cookbook_versions_in_acceptance[1],
718 | }
719 | end
720 |
721 | let(:union_application_versions) do
722 | {
723 | project_app_name => project_app_version,
724 | 'an_application' => '= 3.2.0',
725 | 'another_application' => '= 2.2.4'
726 | }
727 | end
728 |
729 | let(:union_cookbook_versions) do
730 | {
731 | project_cookbook_names[0] => project_cookbook_versions[0],
732 | project_cookbook_names[1] => project_cookbook_versions[1],
733 | 'an_cookbook' => '= 0.3.1',
734 | 'another_cookbook' => '= 2.0.0'
735 | }
736 | end
737 |
738 | before(:each) do
739 | node.default['delivery']['project_cookbooks'] = project_cookbook_names
740 | node.default['delivery']['project_apps'] = [project_app_name]
741 | end
742 |
743 | describe "cached project metadata" do
744 | it "saved all apps and cookbooks for the current project" do
745 | expected_project_metadata = {
746 | project_name => {
747 | 'cookbooks' => project_cookbook_names,
748 | 'applications' => [project_app_name]
749 | }
750 | }
751 |
752 | union_env_result =
753 | described_class.handle_union_pinnings(node, acceptance_env_name, passed_in_project_cookbooks)
754 |
755 | expect(union_env_result.default_attributes['delivery']['project_artifacts']).
756 | to eq(expected_project_metadata)
757 | end
758 | end
759 |
760 | it 'copies cookbook and application version pinnings from the acceptance' \
761 | ' environment to the union environment' do
762 | expected_union_cookbook_versions = union_cookbook_versions.dup
763 | expected_union_cookbook_versions[project_cookbook_names[0]] =
764 | project_cookbook_versions_in_acceptance[0]
765 | expected_union_cookbook_versions[project_cookbook_names[1]] =
766 | project_cookbook_versions_in_acceptance[1]
767 |
768 | expected_union_application_versions = union_application_versions.dup
769 | expected_union_application_versions[project_app_name] =
770 | project_app_version_in_acceptance
771 |
772 | union_env_result =
773 | described_class.handle_union_pinnings(node, acceptance_env_name, [])
774 |
775 | expect(union_env_result.cookbook_versions).
776 | to eq(union_cookbook_versions)
777 | expect(union_env_result.override_attributes['applications']).
778 | to eq(expected_union_application_versions)
779 | end
780 | end
781 | end
782 |
783 | describe '.handle_rehearsal_pinnings' do
784 | let(:rehearsal_applications) do
785 | {
786 | 'app_1' => '0_3_562',
787 | 'app_2' => '1_0_205',
788 | 'no_longer_supported_app' => '0_0_50'
789 | }
790 | end
791 |
792 | let(:rehearsal_cookbook_versions) do
793 | {
794 | 'cookbook_1' => '= 1.2.2',
795 | 'cookbook_2' => '= 0.0.9',
796 | 'no_longer_supported_cookbook' => '= 2.3.0'
797 | }
798 | end
799 |
800 | let(:rehearsal_default_attributes) do
801 | {
802 | 'delivery' => { 'project_artifacts' => {} }
803 | }
804 | end
805 |
806 | let(:union_applications) do
807 | {
808 | 'app_1' => '0_3_563',
809 | 'app_2' => '1_0_206',
810 | 'new_app' => '0_0_1'
811 | }
812 | end
813 |
814 | let(:union_cookbook_versions) do
815 | {
816 | 'cookbook_1' => '= 1.2.3',
817 | 'cookbook_2' => '= 0.1.0',
818 | 'new_cookbook' => '= 0.1.0'
819 | }
820 | end
821 |
822 | let(:union_default_attributes) do
823 | {
824 | 'delivery' => {
825 | 'union_changes' => [
826 | change_id
827 | ],
828 | 'project_artifacts' => {
829 | 'other_project_1' => {
830 | 'cookbooks' => [
831 | 'cookbook_1'
832 | ],
833 | 'applications' => [
834 | 'app_1'
835 | ]
836 | },
837 | 'other_project_2' => {
838 | 'cookbooks' => [
839 | 'cookbook_2'
840 | ],
841 | 'applications' => [
842 | 'app_2'
843 | ]
844 | },
845 | 'new_project' => {
846 | 'cookbooks' => [
847 | 'new_cookbook'
848 | ],
849 | 'applications' => [
850 | 'new_app'
851 | ]
852 | }
853 | }
854 | }
855 | }
856 | end
857 |
858 | let(:rehearsal_env) do
859 | env = Chef::Environment.new
860 | env.name('rehearsal')
861 | env.cookbook_versions(rehearsal_cookbook_versions)
862 | env.default_attributes = rehearsal_default_attributes
863 | env.override_attributes = {
864 | 'applications' => rehearsal_applications
865 | }
866 | env
867 | end
868 |
869 | let(:union_env) do
870 | env = Chef::Environment.new
871 | env.name('union')
872 | env.cookbook_versions(union_cookbook_versions)
873 | env.default_attributes = union_default_attributes
874 | env.override_attributes = {
875 | 'applications' => union_applications
876 | }
877 | env
878 | end
879 |
880 | before(:each) do
881 | expect(Chef::Environment).
882 | to receive(:load).
883 | with('union').
884 | and_return(union_env)
885 | expect(Chef::Environment).
886 | to receive(:load).
887 | with('rehearsal').
888 | and_return(rehearsal_env)
889 | expect(rehearsal_env).
890 | to receive(:save)
891 | expect(union_env).to receive(:save)
892 | end
893 |
894 | it 'removes the change from the union environment change list' do
895 | expect(DeliveryTruck::DeliveryApiClient).
896 | to receive(:blocked_projects).
897 | with(node).
898 | and_return([])
899 |
900 | described_class.handle_rehearsal_pinnings(node)
901 | expect(union_env.default_attributes['delivery']['union_changes']).to eql([])
902 | end
903 |
904 | context 'a project with a single cookbook' do
905 | let(:project_version_in_rehearsal) { "= 2.2.0" }
906 | let(:project_version_in_union) { "= 2.2.2" }
907 |
908 | let(:rehearsal_applications) { {} }
909 | let(:rehearsal_cookbook_versions) do
910 | {
911 | project_name => project_version_in_rehearsal,
912 | 'cookbook_1' => '= 0.3.0',
913 | 'cookbook_2' => '= 1.4.1'
914 | }
915 | end
916 |
917 | let(:union_applications) { {} }
918 | let(:union_cookbook_versions) do
919 | {
920 | project_name => project_version_in_union,
921 | 'cookbook_1' => '= 0.3.1',
922 | 'cookbook_2' => '= 1.4.1'
923 | }
924 | end
925 | let(:rehearsal_default_attributes) do
926 | {
927 | 'delivery' => {
928 | 'project_artifacts' => {
929 | project_name => {
930 | 'cookbooks' => [
931 | project_name,
932 | 'vestigal_cookbook'
933 | ],
934 | 'applications' => []
935 | },
936 | 'other_project_1' => {
937 | 'cookbooks' => [
938 | 'cookbook_1',
939 | 'outdated_cookbook'
940 | ],
941 | 'applications' => []
942 | },
943 | 'other_project_2' => {
944 | 'cookbooks' => [
945 | 'cookbook_2'
946 | ],
947 | 'applications' => []
948 | }
949 | }
950 | }
951 | }
952 | end
953 |
954 | let(:union_default_attributes) do
955 | {
956 | 'delivery' => {
957 | 'project_artifacts' => {
958 | project_name => {
959 | 'cookbooks' => [
960 | project_name
961 | ],
962 | 'applications' => []
963 | },
964 | 'other_project_1' => {
965 | 'cookbooks' => [
966 | 'cookbook_1'
967 | ],
968 | 'applications' => []
969 | },
970 | 'other_project_2' => {
971 | 'cookbooks' => [
972 | 'cookbook_2'
973 | ],
974 | 'applications' => []
975 | }
976 | }
977 | }
978 | }
979 | end
980 |
981 | context 'when the project is blocked' do
982 | before(:each) do
983 | expect(DeliveryTruck::DeliveryApiClient).
984 | to receive(:blocked_projects).
985 | with(node).
986 | and_return([project_name])
987 | end
988 |
989 | it 'does not update the version pinning for the cookbook in the' \
990 | ' rehearsal environment' do
991 | expected_cookbook_versions = {
992 | project_name => project_version_in_rehearsal,
993 | 'cookbook_1' => '= 0.3.1',
994 | 'cookbook_2' => '= 1.4.1'
995 | }
996 |
997 | expected_applications = rehearsal_applications.dup
998 | expected_default_attributes = {
999 | 'delivery' => {
1000 | 'project_artifacts' => {
1001 | project_name => {
1002 | 'cookbooks' => [
1003 | project_name,
1004 | 'vestigal_cookbook'
1005 | ],
1006 | 'applications' => []
1007 | },
1008 | 'other_project_1' => {
1009 | 'cookbooks' => [
1010 | 'cookbook_1'
1011 | ],
1012 | 'applications' => []
1013 | },
1014 | 'other_project_2' => {
1015 | 'cookbooks' => [
1016 | 'cookbook_2'
1017 | ],
1018 | 'applications' => []
1019 | }
1020 | }
1021 | }
1022 | }
1023 |
1024 | rehearsal_env_result = described_class.handle_rehearsal_pinnings(node)
1025 |
1026 | expect(rehearsal_env_result.cookbook_versions).
1027 | to eq(expected_cookbook_versions)
1028 | expect(rehearsal_env_result.default_attributes).
1029 | to eq(expected_default_attributes)
1030 | expect(rehearsal_env_result.override_attributes['applications']).
1031 | to eq(expected_applications)
1032 | end
1033 |
1034 | # maybe we want to test when node['delivery']['project_cookbooks'] is set
1035 | # context 'when the project ships multiple cookbooks' do
1036 | end
1037 |
1038 | context 'when the project is not blocked' do
1039 | let(:blocked_projects) { [] }
1040 | before(:each) do
1041 | expect(DeliveryTruck::DeliveryApiClient).
1042 | to receive(:blocked_projects).
1043 | with(node).
1044 | and_return(blocked_projects)
1045 | end
1046 |
1047 | context 'nothing is blocked' do
1048 | let(:blocked_projects) { [] }
1049 |
1050 | let(:union_applications) do
1051 | {
1052 | "unknown_application" => "1.1.1",
1053 | "other_application" => "0.0.1"
1054 | }
1055 | end
1056 |
1057 | let(:union_cookbook_versions) do
1058 | {
1059 | project_name => project_version_in_union,
1060 | 'cookbook_1' => '= 0.3.1',
1061 | 'cookbook_2' => '= 1.4.1',
1062 | 'unknown_cookbook' => '= 110.100.100'
1063 | }
1064 | end
1065 |
1066 | it 'moves all version pinnings from union to rehersal' do
1067 | expected_cookbook_versions = union_cookbook_versions.dup
1068 | expected_applications = union_applications.dup
1069 | expected_default_attributes = union_default_attributes.dup
1070 |
1071 | rehearsal_env_result = described_class.handle_rehearsal_pinnings(node)
1072 |
1073 | expect(rehearsal_env_result.cookbook_versions).
1074 | to eq(expected_cookbook_versions)
1075 | expect(rehearsal_env_result.default_attributes).
1076 | to eq(expected_default_attributes)
1077 | expect(rehearsal_env_result.override_attributes['applications']).
1078 | to eq(expected_applications)
1079 | end
1080 | end
1081 |
1082 | context 'other project is blocked' do
1083 | let(:blocked_projects) { ['other_project_1'] }
1084 |
1085 | let(:union_default_attributes) do
1086 | {
1087 | 'delivery' => {
1088 | 'project_artifacts' => {
1089 | project_name => {
1090 | 'cookbooks' => [
1091 | project_name
1092 | ],
1093 | 'applications' => []
1094 | },
1095 | 'other_project_1' => {
1096 | 'cookbooks' => [
1097 | 'cookbook_1'
1098 | ],
1099 | 'applications' => []
1100 | },
1101 | 'other_project_2' => {
1102 | 'cookbooks' => [
1103 | 'cookbook_2'
1104 | ],
1105 | 'applications' => []
1106 | }
1107 | }
1108 | }
1109 | }
1110 | end
1111 |
1112 | it 'does not update the version pinning for the impacted cookbook in' \
1113 | ' the rehearsal environment' do
1114 | expected_cookbook_versions = {
1115 | project_name => project_version_in_union,
1116 | 'cookbook_1' => '= 0.3.0',
1117 | 'cookbook_2' => '= 1.4.1' }
1118 | expected_applications = union_applications.dup
1119 | expected_default_attributes = {
1120 | 'delivery' => {
1121 | 'project_artifacts' => {
1122 | project_name => {
1123 | 'cookbooks' => [
1124 | project_name
1125 | ],
1126 | 'applications' => []
1127 | },
1128 | 'other_project_1' => {
1129 | 'cookbooks' => [
1130 | 'cookbook_1',
1131 | 'outdated_cookbook'
1132 | ],
1133 | 'applications' => []
1134 | },
1135 | 'other_project_2' => {
1136 | 'cookbooks' => [
1137 | 'cookbook_2'
1138 | ],
1139 | 'applications' => []
1140 | }
1141 | }
1142 | }
1143 | }
1144 |
1145 | rehearsal_env_result = described_class.handle_rehearsal_pinnings(node)
1146 |
1147 | expect(rehearsal_env_result.cookbook_versions).
1148 | to eq(expected_cookbook_versions)
1149 | expect(rehearsal_env_result.default_attributes).
1150 | to eq(expected_default_attributes)
1151 | expect(rehearsal_env_result.override_attributes['applications']).
1152 | to eq(expected_applications)
1153 | end
1154 | end
1155 | end
1156 | end
1157 |
1158 | context 'a project with several cookbooks' do
1159 | let(:rehearsal_applications) { {} }
1160 | let(:rehearsal_cookbook_versions) do
1161 | {
1162 | 'delivery_1' => '= 0.0.0',
1163 | 'delivery_2' => '= 1.0.0',
1164 | 'cookbook_1' => '= 0.3.0',
1165 | 'cookbook_2' => '= 1.4.1'
1166 | }
1167 | end
1168 | let(:rehearsal_default_attributes) { {
1169 | 'delivery' => { 'project_artifacts' => {} }
1170 | } }
1171 |
1172 | let(:union_applications) { {} }
1173 | let(:union_cookbook_versions) do
1174 | {
1175 | 'delivery_1' => '= 0.0.1',
1176 | 'delivery_2' => '= 1.0.1',
1177 | 'cookbook_1' => '= 0.3.1',
1178 | 'cookbook_2' => '= 1.4.1'
1179 | }
1180 | end
1181 |
1182 | let(:union_default_attributes) do
1183 | {
1184 | 'delivery' => {
1185 | 'project_artifacts' => {
1186 | project_name => {
1187 | 'cookbooks' => [
1188 | 'delivery_1',
1189 | 'delivery_2'
1190 | ],
1191 | 'applications' => []
1192 | },
1193 | 'other_project_1' => {
1194 | 'cookbooks' => [
1195 | 'cookbook_1'
1196 | ],
1197 | 'applications' => []
1198 | },
1199 | 'other_project_2' => {
1200 | 'cookbooks' => [
1201 | 'cookbook_2'
1202 | ],
1203 | 'applications' => []
1204 | }
1205 | }
1206 | }
1207 | }
1208 | end
1209 |
1210 | let(:blocked_projects) { [] }
1211 | let(:node_attributes) do
1212 | {
1213 | 'delivery' => {
1214 | 'change' => {
1215 | 'project' => project_name
1216 | },
1217 | 'project_cookbooks' => ['delivery_1', 'delivery_2']
1218 | }
1219 | }
1220 | end
1221 | before(:each) do
1222 | expect(DeliveryTruck::DeliveryApiClient).
1223 | to receive(:blocked_projects).
1224 | with(node).
1225 | and_return(blocked_projects)
1226 | end
1227 |
1228 | context 'nothing is blocked' do
1229 | let(:blocked_projects) { [] }
1230 |
1231 | it 'updates the version pinning for the cookbook in the rehearsal' \
1232 | ' environment' do
1233 |
1234 | expected_cookbook_versions = union_cookbook_versions.dup
1235 | expected_applications = union_applications.dup
1236 | expected_default_attributes = union_default_attributes.dup
1237 |
1238 | rehearsal_env_result = described_class.handle_rehearsal_pinnings(node)
1239 |
1240 | expect(rehearsal_env_result.cookbook_versions).
1241 | to eq(expected_cookbook_versions)
1242 | expect(rehearsal_env_result.default_attributes).
1243 | to eq(expected_default_attributes)
1244 | expect(rehearsal_env_result.override_attributes['applications']).
1245 | to eq(expected_applications)
1246 | end
1247 |
1248 | context 'when the rehersal delivery attribute has not been initialized' do
1249 | let(:rehearsal_default_attributes) { {} }
1250 |
1251 | it 'properly initializes the hash and the promotes as usual' do
1252 | expected_cookbook_versions = union_cookbook_versions.dup
1253 | expected_applications = union_applications.dup
1254 | expected_default_attributes = union_default_attributes.dup
1255 |
1256 | rehearsal_env_result = described_class.handle_rehearsal_pinnings(node)
1257 |
1258 | expect(rehearsal_env_result.cookbook_versions).
1259 | to eq(expected_cookbook_versions)
1260 | expect(rehearsal_env_result.default_attributes).
1261 | to eq(expected_default_attributes)
1262 | expect(rehearsal_env_result.override_attributes['applications']).
1263 | to eq(expected_applications)
1264 | end
1265 | end
1266 | end
1267 |
1268 | context 'the project is blocked' do
1269 | let(:blocked_projects) { [project_name] }
1270 | it "does not update this project's project cookbooks but does update" \
1271 | "other cookbooks" do
1272 | expected_cookbook_versions = union_cookbook_versions.dup
1273 | expected_cookbook_versions['delivery_1']= '= 0.0.0'
1274 | expected_cookbook_versions['delivery_2']= '= 1.0.0'
1275 |
1276 | expected_applications = union_applications.dup
1277 | expected_default_attributes = rehearsal_default_attributes.dup
1278 |
1279 | rehearsal_env_result = described_class.handle_rehearsal_pinnings(node)
1280 |
1281 | expect(rehearsal_env_result.cookbook_versions).
1282 | to eq(expected_cookbook_versions)
1283 | expect(rehearsal_env_result.default_attributes).
1284 | to eq(expected_default_attributes)
1285 | expect(rehearsal_env_result.override_attributes['applications']).
1286 | to eq(expected_applications)
1287 | end
1288 | end
1289 | end
1290 |
1291 | context 'a project with several cookbooks and applications' do
1292 | let(:rehearsal_applications) do
1293 | {
1294 | 'our_app_1' => '= 2.0.0',
1295 | 'our_app_2' => '= 3.0.0',
1296 | 'app_1' => '= 0.3.0',
1297 | 'app_2' => '= 1.4.1'
1298 | }
1299 | end
1300 | let(:rehearsal_cookbook_versions) do
1301 | {
1302 | 'our_cookbook_1' => '= 0.0.0',
1303 | 'our_cookbook_2' => '= 1.0.0',
1304 | 'cookbook_1' => '= 0.3.0',
1305 | 'cookbook_2' => '= 1.4.1'
1306 | }
1307 | end
1308 |
1309 | let(:union_applications) do
1310 | {
1311 | 'our_app_1' => '= 2.0.1',
1312 | 'our_app_2' => '= 3.0.1',
1313 | 'app_1' => '= 0.3.1',
1314 | 'app_2' => '= 1.4.2'
1315 | }
1316 | end
1317 | let(:union_cookbook_versions) do
1318 | {
1319 | 'our_cookbook_1' => '= 0.0.1',
1320 | 'our_cookbook_2' => '= 1.0.1',
1321 | 'cookbook_1' => '= 0.3.1',
1322 | 'cookbook_2' => '= 1.4.1'
1323 | }
1324 | end
1325 |
1326 | let(:union_default_attributes) do
1327 | {
1328 | 'delivery' => {
1329 | 'project_artifacts' => {
1330 | project_name => {
1331 | 'cookbooks' => [
1332 | 'our_cookbook_1',
1333 | 'our_cookbook_2'
1334 | ],
1335 | 'applications' => [
1336 | 'our_app_1',
1337 | 'our_app_2',
1338 | ]
1339 | },
1340 | 'other_project_1' => {
1341 | 'cookbooks' => [
1342 | 'cookbook_1',
1343 | ],
1344 | 'applications' => [ 'app_1' ]
1345 | },
1346 | 'other_project_2' => {
1347 | 'cookbooks' => [
1348 | 'cookbook_2'
1349 | ],
1350 | 'applications' => [ 'app_2' ]
1351 | }
1352 | }
1353 | }
1354 | }
1355 | end
1356 |
1357 | let(:rehearsal_default_attributes) do
1358 | union_default_attributes.dup
1359 | end
1360 |
1361 | context 'when the project is blocked' do
1362 | before(:each) do
1363 | expect(DeliveryTruck::DeliveryApiClient).
1364 | to receive(:blocked_projects).
1365 | with(node).
1366 | and_return([project_name])
1367 | end
1368 |
1369 | it 'does not update the version pinning for the cookbook or apps in the' \
1370 | ' rehearsal environment' do
1371 | expected_cookbook_versions = {
1372 | 'our_cookbook_1' => '= 0.0.0',
1373 | 'our_cookbook_2' => '= 1.0.0',
1374 | 'cookbook_1' => '= 0.3.1',
1375 | 'cookbook_2' => '= 1.4.1'
1376 | }
1377 |
1378 | expected_applications = {
1379 | 'our_app_1' => '= 2.0.0',
1380 | 'our_app_2' => '= 3.0.0',
1381 | 'app_1' => '= 0.3.1',
1382 | 'app_2' => '= 1.4.2'
1383 | }
1384 |
1385 | rehearsal_env_result = described_class.handle_rehearsal_pinnings(node)
1386 |
1387 | expect(rehearsal_env_result.cookbook_versions).
1388 | to eq(expected_cookbook_versions)
1389 | expect(rehearsal_env_result.override_attributes['applications']).
1390 | to eq(expected_applications)
1391 | end
1392 |
1393 | # maybe we want to test when node['delivery']['project_cookbooks'] is set
1394 | # context 'when the project ships multiple cookbooks' do
1395 | end
1396 |
1397 | context 'when a different project is blocked' do
1398 | before(:each) do
1399 | expect(DeliveryTruck::DeliveryApiClient).
1400 | to receive(:blocked_projects).
1401 | with(node).
1402 | and_return(['other_project_1'])
1403 | end
1404 |
1405 | it 'does not update the version pinning for the cookbook or apps in the' \
1406 | ' rehearsal environment' do
1407 | expected_cookbook_versions = {
1408 | 'our_cookbook_1' => '= 0.0.1',
1409 | 'our_cookbook_2' => '= 1.0.1',
1410 | 'cookbook_1' => '= 0.3.0',
1411 | 'cookbook_2' => '= 1.4.1'
1412 | }
1413 |
1414 | expected_applications = {
1415 | 'our_app_1' => '= 2.0.1',
1416 | 'our_app_2' => '= 3.0.1',
1417 | 'app_1' => '= 0.3.0',
1418 | 'app_2' => '= 1.4.2'
1419 | }
1420 |
1421 | rehearsal_env_result = described_class.handle_rehearsal_pinnings(node)
1422 |
1423 | expect(rehearsal_env_result.cookbook_versions).
1424 | to eq(expected_cookbook_versions)
1425 | expect(rehearsal_env_result.override_attributes['applications']).
1426 | to eq(expected_applications)
1427 | end
1428 |
1429 | # maybe we want to test when node['delivery']['project_cookbooks'] is set
1430 | # context 'when the project ships multiple cookbooks' do
1431 | end
1432 |
1433 | end
1434 | end
1435 |
1436 | describe '.handle_delivered_pinnings' do
1437 | let(:previous_stage_env_name) { 'rehearsal' }
1438 |
1439 | let(:previous_stage_applications) do
1440 | {
1441 | 'app_1' => '0_3_563',
1442 | 'app_2' => '1_0_206',
1443 | 'new_app' => '0_0_1'
1444 | }
1445 | end
1446 |
1447 | let(:previous_stage_cookbook_versions) do
1448 | {
1449 | 'cookbook_1' => '= 1.2.3',
1450 | 'cookbook_2' => '= 0.1.0',
1451 | 'new_cookbook' => '= 0.1.0'
1452 | }
1453 | end
1454 |
1455 | let(:previous_stage_default_attributes) do
1456 | {
1457 | 'foo' => 'bar'
1458 | }
1459 | end
1460 |
1461 | let(:previous_stage_env) do
1462 | env = Chef::Environment.new()
1463 | env.name(previous_stage_env_name)
1464 | env.cookbook_versions(previous_stage_cookbook_versions)
1465 | env.default_attributes = previous_stage_default_attributes
1466 | env.override_attributes = {
1467 | 'applications' => previous_stage_applications
1468 | }
1469 | env
1470 | end
1471 |
1472 | let(:current_stage_env_name) { 'delivered' }
1473 |
1474 | let(:current_stage_applications) do
1475 | {
1476 | 'app_1' => '0_3_562',
1477 | 'app_2' => '1_0_205',
1478 | 'no_longer_supported_app' => '0_0_50'
1479 | }
1480 | end
1481 |
1482 | let(:current_stage_cookbook_versions) do
1483 | {
1484 | 'cookbook_1' => '= 1.2.2',
1485 | 'cookbook_2' => '= 0.0.9',
1486 | 'no_longer_supported_cookbook' => '= 2.3.0'
1487 | }
1488 | end
1489 |
1490 | let(:current_stage_default_attributes) do
1491 | {
1492 | 'foo' => 'baz'
1493 | }
1494 | end
1495 |
1496 | let(:current_stage_env) do
1497 | env = Chef::Environment.new()
1498 | env.name(current_stage_env_name)
1499 | env.cookbook_versions(current_stage_cookbook_versions)
1500 | env.default_attributes = current_stage_default_attributes
1501 | env.override_attributes = {
1502 | 'applications' => current_stage_applications
1503 | }
1504 | env
1505 | end
1506 |
1507 | before(:each) do
1508 | expect(Chef::Environment).
1509 | to receive(:load).
1510 | with(previous_stage_env_name).
1511 | and_return(previous_stage_env)
1512 | expect(Chef::Environment).
1513 | to receive(:load).
1514 | with(current_stage_env_name).
1515 | and_return(current_stage_env)
1516 | expect(current_stage_env).
1517 | to receive(:save)
1518 | end
1519 |
1520 | it 'merges all cookbook and application version pinnings from the previous' \
1521 | ' environment to the current environment' do
1522 | expected_cookbook_versions = previous_stage_cookbook_versions.dup
1523 |
1524 | expected_applications = previous_stage_applications.dup
1525 |
1526 | expected_default_attributes = previous_stage_default_attributes.dup
1527 |
1528 | current_stage_env_result =
1529 | described_class.handle_delivered_pinnings(node)
1530 |
1531 | expect(current_stage_env_result.cookbook_versions).
1532 | to eq(expected_cookbook_versions)
1533 | expect(current_stage_env_result.default_attributes).
1534 | to eq(expected_default_attributes)
1535 | expect(current_stage_env_result.override_attributes['applications']).
1536 | to eq(expected_applications)
1537 | end
1538 | end
1539 |
1540 | end
1541 |
--------------------------------------------------------------------------------