├── .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 | --------------------------------------------------------------------------------