├── .rspec ├── spec ├── unit │ ├── fixtures │ │ ├── nodes │ │ │ └── demo.json │ │ ├── roles │ │ │ └── base.json │ │ ├── environments │ │ │ └── dev.json │ │ ├── cookbooks │ │ │ ├── dummy │ │ │ │ └── metadata.rb │ │ │ └── nginx │ │ │ │ └── metadata.rb │ │ ├── .chef │ │ │ ├── encrypted_data_bag_secret │ │ │ ├── validator.pem │ │ │ └── trusted_certs │ │ │ │ └── chef_example_com.crt │ │ ├── site-cookbooks │ │ │ └── apt │ │ │ │ └── metadata.rb │ │ ├── Berksfile │ │ └── data_bags │ │ │ └── secrets │ │ │ └── passwords.json │ ├── helpers_spec.rb │ ├── container_docker_rebuild_spec.rb │ ├── container_docker_init_spec.rb │ └── container_docker_build_spec.rb ├── functional │ ├── fixtures │ │ └── ohai │ │ │ └── Dockerfile │ └── docker_container_ohai_spec.rb ├── spec_helper.rb └── test_helpers.rb ├── lib ├── knife-container │ ├── skeletons │ │ └── knife_container │ │ │ ├── templates │ │ │ └── default │ │ │ │ ├── gitignore.erb │ │ │ │ ├── dockerignore.erb │ │ │ │ ├── node_name.erb │ │ │ │ ├── berksfile.erb │ │ │ │ ├── dockerfile.erb │ │ │ │ └── config.rb.erb │ │ │ ├── metadata.rb │ │ │ ├── files │ │ │ └── default │ │ │ │ └── plugins │ │ │ │ └── docker_container.rb │ │ │ └── recipes │ │ │ └── docker_init.rb │ ├── version.rb │ ├── helpers │ │ ├── berkshelf.rb │ │ └── docker.rb │ ├── helpers.rb │ ├── command.rb │ ├── chef_runner.rb │ └── generator.rb └── chef │ └── knife │ ├── container_docker_base.rb │ ├── container_docker_rebuild.rb │ ├── container_docker_init.rb │ └── container_docker_build.rb ├── .travis.yml ├── Gemfile ├── .gitignore ├── Rakefile ├── knife-container.gemspec ├── CHANGELOG.md ├── README.md ├── CONTRIBUTING.md └── LICENSE /.rspec: -------------------------------------------------------------------------------- 1 | -f doc --color 2 | -------------------------------------------------------------------------------- /spec/unit/fixtures/nodes/demo.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/unit/fixtures/roles/base.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/unit/fixtures/environments/dev.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/unit/fixtures/cookbooks/dummy/metadata.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/unit/fixtures/cookbooks/nginx/metadata.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/unit/fixtures/.chef/encrypted_data_bag_secret: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/unit/fixtures/site-cookbooks/apt/metadata.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/unit/fixtures/.chef/validator.pem: -------------------------------------------------------------------------------- 1 | E_NOTAREALKEY 2 | -------------------------------------------------------------------------------- /spec/unit/fixtures/.chef/trusted_certs/chef_example_com.crt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/knife-container/skeletons/knife_container/templates/default/gitignore.erb: -------------------------------------------------------------------------------- 1 | chef/secure/* 2 | -------------------------------------------------------------------------------- /lib/knife-container/skeletons/knife_container/templates/default/dockerignore.erb: -------------------------------------------------------------------------------- 1 | chef/secure_backup 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 2.0.0 3 | - 2.1.1 4 | script: bundle exec rspec --color --format progress 5 | -------------------------------------------------------------------------------- /spec/unit/fixtures/Berksfile: -------------------------------------------------------------------------------- 1 | source "https://supermarket.getchef.com" 2 | 3 | cookbook "nginx", "= 2.7.4" 4 | -------------------------------------------------------------------------------- /lib/knife-container/version.rb: -------------------------------------------------------------------------------- 1 | module Knife 2 | module Container 3 | VERSION = '0.3.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/functional/fixtures/ohai/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM chef/ubuntu_12.04:0.1.0 2 | ADD chef /chef/ 3 | RUN chef-init --provision 4 | -------------------------------------------------------------------------------- /lib/knife-container/skeletons/knife_container/templates/default/node_name.erb: -------------------------------------------------------------------------------- 1 | <%= dockerfile_name.gsub('/','-') %>-build 2 | -------------------------------------------------------------------------------- /spec/unit/fixtures/data_bags/secrets/passwords.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "passwords", 3 | "twitter": "biz", 4 | "facebook": "mark" 5 | } 6 | -------------------------------------------------------------------------------- /lib/knife-container/skeletons/knife_container/templates/default/berksfile.erb: -------------------------------------------------------------------------------- 1 | source "<%= @berksfile_source %>" 2 | 3 | <% @cookbooks.each do |cookbook| -%> 4 | cookbook "<%= cookbook %>" 5 | <% end -%> 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in knife-container.gemspec 4 | gemspec 5 | 6 | gem 'coveralls', require: false 7 | 8 | group :spec do 9 | gem 'berkshelf', '~> 3.1.1' 10 | end 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | vendor/* 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |t| 5 | t.rspec_opts = [].tap do |a| 6 | a.push('--color') 7 | a.push('--format doc') 8 | end.join(' ') 9 | end 10 | 11 | desc 'Run all tests' 12 | task :test => [:spec] 13 | 14 | 15 | task :default => [:test] 16 | -------------------------------------------------------------------------------- /lib/knife-container/skeletons/knife_container/metadata.rb: -------------------------------------------------------------------------------- 1 | name 'knife_container' 2 | maintainer 'Chef Software, Inc.' 3 | maintainer_email 'dev@getchef.com' 4 | license 'Apache 2 License' 5 | description 'Generates Chef code for knife-container' 6 | long_description 'Generates Chef code for knife-container' 7 | version '0.1.0' 8 | -------------------------------------------------------------------------------- /lib/knife-container/skeletons/knife_container/templates/default/dockerfile.erb: -------------------------------------------------------------------------------- 1 | # BASE <%= base_image %> 2 | FROM <%= dockerfile_name %> 3 | ADD chef/ /etc/chef/ 4 | <% if include_credentials -%> 5 | RUN chef-init --bootstrap --no-remove-secure 6 | <% else -%> 7 | RUN chef-init --bootstrap 8 | <% end -%> 9 | ENTRYPOINT ["chef-init"] 10 | CMD ["--onboot"] 11 | -------------------------------------------------------------------------------- /lib/knife-container/skeletons/knife_container/templates/default/config.rb.erb: -------------------------------------------------------------------------------- 1 | require 'chef-init' 2 | 3 | node_name ChefInit.node_name 4 | <% if chef_client_mode == "zero" -%> 5 | cookbook_path ["/etc/chef/cookbooks"] 6 | <% elsif chef_client_mode == "client" -%> 7 | chef_server_url '<%= chef_server_url %>' 8 | validation_client_name '<%= validation_client_name %>' 9 | validation_key '/etc/chef/secure/validation.pem' 10 | client_key '/etc/chef/secure/client.pem' 11 | trusted_certs_dir '/etc/chef/secure/trusted_certs' 12 | <% end -%> 13 | <% unless encrypted_data_bag_secret.nil? -%> 14 | encrypted_data_bag_secret '/etc/chef/secure/encrypted_data_bag_secret' 15 | <% end -%> 16 | ssl_verify_mode :verify_peer 17 | -------------------------------------------------------------------------------- /spec/functional/docker_container_ohai_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 | describe 'docker_container Ohai plugin' do 19 | 20 | end 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'test_helpers' 19 | 20 | RSpec.configure do |c| 21 | c.include TestHelpers 22 | 23 | c.expect_with :rspec do |config| 24 | config.syntax = [:should, :expect] 25 | end 26 | c.filter_run :focus => true 27 | c.run_all_when_everything_filtered = true 28 | c.treat_symbols_as_metadata_keys_with_true_values = true 29 | end 30 | -------------------------------------------------------------------------------- /lib/chef/knife/container_docker_base.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'chef/knife' 19 | require 'knife-container/command' 20 | require 'knife-container/helpers' 21 | require 'chef/json_compat' 22 | 23 | class Chef 24 | class Knife 25 | module ContainerDockerBase 26 | include KnifeContainer::Command 27 | include KnifeContainer::Helpers 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/knife-container/helpers/berkshelf.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 | require 'mkmf' 18 | 19 | module KnifeContainer 20 | module Helpers 21 | module Berkshelf 22 | 23 | # 24 | # Determines whether Berkshelf is installed 25 | # 26 | # @returns [TrueClass, FalseClass] 27 | # 28 | def berks_installed? 29 | ! ::MakeMakefile.find_executable('berks').nil? 30 | end 31 | 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/knife-container/helpers.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 | require 'knife-container/helpers/berkshelf' 18 | require 'knife-container/helpers/docker' 19 | 20 | module KnifeContainer 21 | module Helpers 22 | include KnifeContainer::Helpers::Berkshelf 23 | include KnifeContainer::Helpers::Docker 24 | 25 | # 26 | # Generates a short, but random UID for instances. 27 | # 28 | # @return [String] 29 | # 30 | def random_uid 31 | require 'securerandom' unless defined?(SecureRandom) 32 | SecureRandom.hex(3) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/knife-container/skeletons/knife_container/files/default/plugins/docker_container.rb: -------------------------------------------------------------------------------- 1 | require 'docker' # it gets this from chef-init 2 | 3 | Ohai.plugin(:DockerContainer) do 4 | provides "docker_container" 5 | 6 | def container_id 7 | shell_out("hostname").stdout.strip 8 | end 9 | 10 | def looks_like_docker? 11 | hint?('docker_container') || !!Docker.version && !!Docker::Container.get(container_id) 12 | end 13 | 14 | ## 15 | # The format of the data is collection is the inspect API 16 | # http://docs.docker.io/reference/api/docker_remote_api_v1.11/#inspect-a-container 17 | # 18 | collect_data do 19 | metadata_from_hints = hint?('docker_container') 20 | 21 | if looks_like_docker? 22 | Ohai::Log.debug("looks_like_docker? == true") 23 | docker_container Mash.new 24 | 25 | if metadata_from_hints 26 | Ohai::Log.debug("docker_container hints present") 27 | metadata_from_hints.each { |k,v| docker_container[k] = v } 28 | end 29 | 30 | container = Docker::Container.get(container_id).json 31 | container.each { |k,v| docker_container[k] = v } 32 | else 33 | Ohai::Log.debug("looks_like_docker? == false") 34 | false 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /knife-container.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'knife-container/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'knife-container' 8 | spec.version = Knife::Container::VERSION 9 | spec.authors = ['Tom Duffield'] 10 | spec.email = ['tom@getchef.com'] 11 | spec.summary = %q(Container support for Chef's Knife Command) 12 | spec.description = spec.summary 13 | spec.homepage = 'http://github.com/opscode/knife-container' 14 | spec.license = 'Apache 2.0' 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.required_ruby_version = '>= 2.0' 22 | 23 | spec.add_dependency 'chef', '>= 11.16.0' 24 | spec.add_dependency 'docker-api', '~> 1.11.1' 25 | 26 | %w(rspec-core rspec-expectations rspec-mocks).each { |gem| spec.add_development_dependency gem, '~> 2.14.0' } 27 | spec.add_development_dependency 'bundler', '~> 1.3' 28 | spec.add_development_dependency 'rake', '~> 10.1.0' 29 | spec.add_development_dependency 'pry' 30 | end 31 | -------------------------------------------------------------------------------- /lib/knife-container/command.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'knife-container/generator' 19 | require 'knife-container/chef_runner' 20 | 21 | module KnifeContainer 22 | module Command 23 | 24 | # An instance of ChefRunner. Calling ChefRunner#converge will trigger 25 | # convergence and generate the desired code. 26 | def chef_runner 27 | @chef_runner ||= ChefRunner.new(docker_cookbook_path, ["knife_container::#{recipe}"]) 28 | end 29 | 30 | # Path to the directory where the code_generator cookbook is located. 31 | # For now, this is hard coded to the 'skeletons' directory in this 32 | # repo. 33 | def docker_cookbook_path 34 | File.expand_path('../skeletons', __FILE__) 35 | end 36 | 37 | # Delegates to `Generator.context`, the singleton instance of 38 | # Generator::Context 39 | def generator_context 40 | Generator.context 41 | end 42 | 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/test_helpers.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'fileutils' 19 | require 'tmpdir' 20 | 21 | module TestHelpers 22 | 23 | # A globally accessible place where we can put some state to verify that a 24 | # test performed a certain operation. 25 | def self.test_state 26 | @test_state ||= {} 27 | end 28 | 29 | def self.reset! 30 | @test_state = nil 31 | end 32 | 33 | def test_state 34 | TestHelpers.test_state 35 | end 36 | 37 | def fixtures_path 38 | File.expand_path(File.dirname(__FILE__) + "/unit/fixtures/") 39 | end 40 | 41 | def project_root 42 | File.expand_path("../..", __FILE__) 43 | end 44 | 45 | def reset_tempdir 46 | clear_tempdir 47 | tempdir 48 | end 49 | 50 | def clear_tempdir 51 | FileUtils.rm_rf(@tmpdir) 52 | @tmpdir = nil 53 | end 54 | 55 | def tempdir 56 | @tmpdir ||= Dir.mktmpdir("knife-container") 57 | File.realpath(@tmpdir) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Knife Container Changelog 2 | 3 | ## v0.3.0 (unreleased) 4 | * Added 'docker container rebuild' subcommand 5 | * Added `Chef::Config[:knife][:docker_image]` configuration value to allow for the 6 | specification of what the default Docker Image should be. The default value is 7 | `chef/ubuntu-12.04:latest` 8 | * Added `Chef::Config[:knife][:berksfile_source]` configuration value to allow for 9 | specification of which source you'd like to use in a generated Berksfile. The 10 | default value is `https://supermarket.getchef.com`. 11 | * [GH-6] Use supermarket as the default Berkshelf source. 12 | * [FSE-188] Method for stripping secure credentials resulted in intermediate 13 | image with those credentials still present. Stripping out those intermediate 14 | layers is now the responsibility of `chef-init --bootstrap`. Reported by Andrew 15 | Hsu. 16 | * Docker commands are now done directly via Docker API instead of shelling out 17 | to Docker CLI. 18 | * [GH-27] Added `--data-bag-path` option. Copies over data bags. 19 | * [FSE-201] Update knife-container to support ChefDK and Chef 12 libraries 20 | 21 | ## v0.2.3 (2014-09-19) 22 | * [GH-39] Fixed `--dockerfiles-path` parameter which did not properly accept input. 23 | * [GH-38] Fixed issue where cookbooks would appear more than once in the Berksfile. 24 | 25 | ## v0.2.2 (2014-09-08) 26 | * [GH-34] Update gemspec to support Chef12. 27 | 28 | ## v0.2.1 (2014-08-15) 29 | * [GH-23] Specify hostname during knife container build 30 | 31 | ## v0.2.0 (2014-07-16) 32 | * `knife container docker init` - Initialize a Docker context on your local workstation. 33 | * `knife container docker build` - Build a Docker image on your local workstation. 34 | -------------------------------------------------------------------------------- /lib/knife-container/chef_runner.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'chef' 19 | 20 | module KnifeContainer 21 | # An adapter to chef's APIs to kick off a chef-client run. 22 | class ChefRunner 23 | 24 | attr_reader :cookbook_path 25 | attr_reader :run_list 26 | 27 | def initialize(cookbook_path, run_list) 28 | @cookbook_path = cookbook_path 29 | @run_list = run_list 30 | @formatter = nil 31 | @ohai = nil 32 | end 33 | 34 | def converge 35 | configure 36 | Chef::Runner.new(run_context).converge 37 | end 38 | 39 | def run_context 40 | @run_context ||= policy.setup_run_context 41 | end 42 | 43 | def policy 44 | return @policy_builder if @policy_builder 45 | 46 | @policy_builder = Chef::PolicyBuilder::ExpandNodeObject.new("knife_container", ohai.data, {}, nil, formatter) 47 | @policy_builder.load_node 48 | @policy_builder.build_node 49 | @policy_builder.node.run_list(*run_list) 50 | @policy_builder.expand_run_list 51 | @policy_builder 52 | end 53 | 54 | def formatter 55 | @formatter ||= Chef::Formatters.new(:doc, stdout, stderr) 56 | end 57 | 58 | def configure 59 | Chef::Config.solo = true 60 | Chef::Config.cookbook_path = cookbook_path 61 | Chef::Config.color = true 62 | Chef::Config.diff_disabled = true 63 | end 64 | 65 | def ohai 66 | return @ohai if @ohai 67 | 68 | @ohai = Ohai::System.new 69 | @ohai.all_plugins(['platform', 'platform_version']) 70 | @ohai 71 | end 72 | 73 | def stdout 74 | $stdout 75 | end 76 | 77 | def stderr 78 | $stderr 79 | end 80 | 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/unit/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'spec_helper' 19 | require 'knife-container/helpers' 20 | 21 | describe KnifeContainer::Helpers do 22 | 23 | subject(:klass) { 24 | class DummyClass;include KnifeContainer::Helpers;end 25 | DummyClass.new 26 | } 27 | 28 | describe '.valid_dockerfile_name?' do 29 | let(:names) { Hash.new( 30 | 'http://reg.example.com/image_name-test' => false, 31 | 'http://reg.example.com:1234/image_name-test:tag' => false, 32 | 'reg.example.com/image_name-test:tag' => false, 33 | 'image_name-test:tag' => false, 34 | 'image_name-test' => true 35 | )} 36 | 37 | it 'returns whether the name meets specified criteria' do 38 | names.each do |name, value| 39 | expect(klass.valid_dockerfile_name?(name)).to eq(value) 40 | end 41 | end 42 | end 43 | 44 | describe '.parse_dockerfile_name' do 45 | let(:input_values) { %w[ 46 | reg.example.com:1234/image_name-test 47 | reg.example.com/image_name-test 48 | example.com:1234/image_name-test 49 | example.com/image_name-test 50 | example/image_name-test 51 | image_name-test 52 | ]} 53 | 54 | let(:output_values) { %w[ 55 | reg_example_com_1234/image_name-test 56 | reg_example_com/image_name-test 57 | example_com_1234/image_name-test 58 | example_com/image_name-test 59 | example/image_name-test 60 | image_name-test 61 | ]} 62 | 63 | it 'replaces special characters in dockerfile names' do 64 | i = 0 65 | num = input_values.length 66 | 67 | while i < num do 68 | expect(klass.parse_dockerfile_name(input_values[i])).to eql(output_values[i]) 69 | i += 1 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/knife-container/generator.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 KnifeContainer 19 | 20 | module Generator 21 | 22 | class Context 23 | 24 | attr_accessor :dockerfile_name 25 | attr_accessor :dockerfiles_path 26 | attr_accessor :base_image 27 | attr_accessor :chef_client_mode 28 | attr_accessor :run_list 29 | attr_accessor :cookbook_path 30 | attr_accessor :role_path 31 | attr_accessor :node_path 32 | attr_accessor :data_bag_path 33 | attr_accessor :environment_path 34 | attr_accessor :chef_server_url 35 | attr_accessor :validation_key 36 | attr_accessor :validation_client_name 37 | attr_accessor :trusted_certs_dir 38 | attr_accessor :encrypted_data_bag_secret 39 | attr_accessor :first_boot 40 | attr_accessor :berksfile 41 | attr_accessor :berksfile_source 42 | attr_accessor :generate_berksfile 43 | attr_accessor :run_berks 44 | attr_accessor :force_build 45 | attr_accessor :include_credentials 46 | 47 | end 48 | 49 | def self.reset 50 | @context = nil 51 | end 52 | 53 | def self.context 54 | @context ||= Context.new 55 | end 56 | 57 | module TemplateHelper 58 | 59 | def self.delegate_to_app_context(name) 60 | define_method(name) do 61 | KnifeContainer::Generator.context.public_send(name) 62 | end 63 | end 64 | 65 | # delegate all the attributes of app_config 66 | delegate_to_app_context :dockerfile_name 67 | delegate_to_app_context :dockerfiles_path 68 | delegate_to_app_context :base_image 69 | delegate_to_app_context :chef_client_mode 70 | delegate_to_app_context :run_list 71 | delegate_to_app_context :cookbook_path 72 | delegate_to_app_context :role_path 73 | delegate_to_app_context :node_path 74 | delegate_to_app_context :environment_path 75 | delegate_to_app_context :chef_server_url 76 | delegate_to_app_context :validation_key 77 | delegate_to_app_context :validation_client_name 78 | delegate_to_app_context :trusted_certs_dir 79 | delegate_to_app_context :encrypted_data_bag_secret 80 | delegate_to_app_context :first_boot 81 | delegate_to_app_context :berksfile 82 | delegate_to_app_context :berksfile_source 83 | delegate_to_app_context :generate_berksfile 84 | delegate_to_app_context :run_berks 85 | delegate_to_app_context :force_build 86 | delegate_to_app_context :include_credentials 87 | 88 | end 89 | 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Knife Container 2 | [![Gem Version](https://badge.fury.io/rb/knife-container.png)](http://badge.fury.io/rb/knife-container) 3 | [![Build Status](https://travis-ci.org/chef/knife-container.svg?branch=master)](https://travis-ci.org/chef/knife-container) 4 | 5 | **This project is deprecated in favor of the [Docker driver inside Chef Provisioning](https://github.com/chef/chef-provisioning-docker), and no longer maintained.** 6 | 7 | This is the official Chef plugin for Linux Containers. This plugin gives knife 8 | the ability to initialize and build Linux Containers. 9 | 10 | For full documentation, including examples, please check out [the docs site](https://docs.chef.io/plugin_knife_container.html). 11 | 12 | ## Installation 13 | 14 | ### Build Locally 15 | If you would like to build the gem from source locally, please clone this 16 | repository on to your local machine and build the gem locally. 17 | 18 | ``` 19 | $ bundle install 20 | $ bundle exec rake install 21 | ``` 22 | 23 | ## Subcommands 24 | This plugin provides the following Knife subcommands. Specific command options 25 | can be found by invoking the subcommand with a `--help` flag. 26 | 27 | #### `knife container docker init` 28 | Initializes a new folder that will hold all the files and folders necessary to 29 | build a Docker image called a “Docker context.” This files and folders that can 30 | make up your Docker context include a Dockerfile, Berksfile, cookbooks and 31 | chef-client configuration files. 32 | 33 | #### `knife container docker build` 34 | Builds a Docker image based on the Docker context specified. If the image was 35 | initialized using the `-z` flag and a Berksfile exists, it will run `berks vendor` 36 | and vendor the required cookbooks into the required directory. If the image was 37 | initialized without the `-z` flag and a Berksfile exists, it will run 38 | `berks upload` and upload the required cookbooks to you Chef Server. 39 | 40 | ## Configuration 41 | This plugin allows certain values to be specified in your `knife.rb` 42 | 43 | ### `knife[:dockerfiles_path]` 44 | Allows you to specify the directory where you wish to keep your Docker Contexts. 45 | By default this value is a folder named `dockerfiles` folder in your chef-repo 46 | directory. 47 | 48 | ### `knife[:docker_image]` 49 | Allows you to specify what Docker Image should be used if the `-f` flag is not 50 | specified when you run `knife container docker init`. The default value is 51 | `chef/ubuntu-12.04:latest`. 52 | 53 | ### `knife[:berksfile_source]` 54 | Allows you to specify the source you wish to use in your generated Berksfiles. 55 | The default value is `https://supermarket.chef.io`. 56 | 57 | ## Contributing 58 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) 59 | 60 | ## License 61 | Full License: [here](LICENSE) 62 | 63 | Knife-Container - a Knife plugin for chef-container 64 | 65 | Author:: Tom Duffield () 66 | Author:: Michael Goetz () 67 | 68 | Copyright:: Copyright (c) 2012-2014 Chef Software, Inc. 69 | License:: Apache License, Version 2.0 70 | 71 | Licensed under the Apache License, Version 2.0 (the "License"); 72 | you may not use this file except in compliance with the License. 73 | You may obtain a copy of the License at 74 | 75 | http://www.apache.org/licenses/LICENSE-2.0 76 | 77 | Unless required by applicable law or agreed to in writing, software 78 | distributed under the License is distributed on an "AS IS" BASIS, 79 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 80 | See the License for the specific language governing permissions and 81 | limitations under the License. 82 | -------------------------------------------------------------------------------- /lib/knife-container/helpers/docker.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 | require 'docker' 18 | require 'chef/json_compat' 19 | 20 | module KnifeContainer 21 | module Helpers 22 | module Docker 23 | 24 | # 25 | # Determines whether the Docker image name the user gave is valid. 26 | # 27 | # @param name [String] the Dockerfile name 28 | # 29 | # @return [TrueClass, FalseClass] whether the Dockerfile name is valid 30 | # 31 | def valid_dockerfile_name?(name) 32 | case 33 | when name.match(/:([a-zA-Z0-9._\-]+)?$/) # Does it have a tag? 34 | false 35 | when name.match(/^\w+:\/\//) # Does it include a protocol? 36 | false 37 | else 38 | true 39 | end 40 | end 41 | 42 | # 43 | # Converts the dockerfile name into something safe 44 | # 45 | def parse_dockerfile_name(name) 46 | name.gsub(/[\.\:]/, '_') 47 | end 48 | 49 | # 50 | # Downloads the specified Docker Image from the Registry 51 | # TODO: print out status 52 | # 53 | def download_image(image_name) 54 | ui.info("Downloading #{image_name}") 55 | name, tag = image_name.split(':') 56 | if tag.nil? 57 | img = ::Docker::Image.create(:fromImage => name) 58 | else 59 | img = ::Docker::Image.create(:fromImage => name, :tag => tag) 60 | end 61 | img.id 62 | rescue Excon::Errors::SocketError => e 63 | ui.fatal(connection_error) 64 | exit 1 65 | end 66 | 67 | # 68 | # Build Docker Image 69 | # 70 | def build_image(dir) 71 | ui.info("Building image based on Dockerfile in #{dir}") 72 | img = ::Docker::Image.build_from_dir(dir) do |output| 73 | log = Chef::JSONCompat.new.parse(output) 74 | puts log['stream'] 75 | end 76 | rescue Excon::Errors::SocketError => e 77 | ui.fatal(connection_error) 78 | exit 1 79 | end 80 | 81 | 82 | # 83 | # Delete the specified image 84 | # 85 | def delete_image(image_name) 86 | ui.info("Deleting Docker image #{image_name}") 87 | image = ::Docker::Image.get(image_name) 88 | image.remove 89 | rescue Excon::Errors::SocketError => e 90 | ui.fatal(connection_error) 91 | exit 1 92 | end 93 | 94 | # 95 | # Tag the specified Docker Image 96 | # 97 | def tag_image(image_id, image_name, tag='latest') 98 | ui.info("Add tag #{image_name}:#{tag} to #{image_id}") 99 | image = ::Docker::Image.get(image_id) 100 | image.tag(:repo => image_name, :tag => tag) 101 | rescue Excon::Errors::SocketError => e 102 | ui.fatal(connection_error) 103 | exit 1 104 | end 105 | 106 | def connection_error 107 | 'Could not connect to Docker API. Please make sure your Docker daemon '\ 108 | 'process is running. If you are using boot2docker, please ensure that '\ 109 | 'your VM is up and started.' 110 | end 111 | 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/unit/container_docker_rebuild_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'spec_helper' 19 | require 'chef/knife/container_docker_rebuild' 20 | Chef::Knife::ContainerDockerRebuild.load_deps 21 | 22 | describe Chef::Knife::ContainerDockerRebuild do 23 | 24 | let(:stdout_io) { StringIO.new } 25 | let(:stderr_io) { StringIO.new } 26 | 27 | def stdout 28 | stdout_io.string 29 | end 30 | 31 | let(:default_dockerfiles_path) do 32 | File.expand_path("dockerfiles", fixtures_path) 33 | end 34 | 35 | subject(:knife) do 36 | Chef::Knife::ContainerDockerRebuild.new(argv).tap do |c| 37 | c.stub(:output).and_return(true) 38 | c.parse_options(argv) 39 | c.merge_configs 40 | end 41 | end 42 | 43 | describe '#run' do 44 | let(:argv) { %w[ docker/demo ] } 45 | 46 | it 'parses arguments, redownload and retags the docker image, then runs DockerBuild' do 47 | expect(knife).to receive(:validate).and_call_original 48 | expect(knife).to receive(:setup_config_defaults).and_call_original 49 | expect(knife).to receive(:redownload_docker_image) 50 | expect(knife).to receive(:run_build_image) 51 | knife.run 52 | end 53 | end 54 | 55 | describe '#redownload_docker_image' do 56 | let(:argv) { %w[ docker/demo ] } 57 | 58 | before do 59 | allow(knife).to receive(:parse_dockerfile_for_base).and_return('chef/ubuntu-12.04:latest') 60 | allow(knife).to receive(:download_image).and_return('0123456789ABCDEF') 61 | end 62 | 63 | it 'parses the Dockerfile for BASE and pulls down that image' do 64 | expect(knife).to receive(:parse_dockerfile_for_base).and_return('chef/ubuntu-12.04:latest') 65 | expect(knife).to receive(:delete_image).with('docker/demo') 66 | expect(knife).to receive(:download_image).with('chef/ubuntu-12.04:latest').and_return('0123456789ABCDEF') 67 | expect(knife).to receive(:tag_image).with('0123456789ABCDEF', 'docker/demo') 68 | knife.redownload_docker_image 69 | end 70 | end 71 | 72 | describe '#parse_dockerfile_for_base' do 73 | let(:argv) { %w[ docker/demo ] } 74 | let(:valid_file_contents) { StringIO.new("# BASE chef/ubuntu-14.04:latest\nFROM docker/demo") } 75 | let(:invalid_file_contents) { StringIO.new("FROM docker/demo") } 76 | 77 | before { allow(File).to receive(:open).and_return(valid_file_contents) } 78 | 79 | it 'returns the BASE value from the file' do 80 | expect(knife.parse_dockerfile_for_base).to eql('chef/ubuntu-14.04:latest') 81 | end 82 | 83 | context 'when BASE is missing' do 84 | before do 85 | Chef::Config[:knife][:docker_image] = 'chef/centos-6:latest' 86 | allow(File).to receive(:open).and_return(invalid_file_contents) 87 | end 88 | 89 | it 'returns the default Chef::Config value' do 90 | expect(knife.parse_dockerfile_for_base).to eql('chef/centos-6:latest') 91 | end 92 | 93 | context 'and Chef::Config value is missing' do 94 | before do 95 | Chef::Config[:knife][:docker_image] = nil 96 | allow(File).to receive(:open).and_return(invalid_file_contents) 97 | end 98 | 99 | it 'returns chef/ubuntu-12.04' do 100 | expect(knife.parse_dockerfile_for_base).to eql('chef/ubuntu-12.04:latest') 101 | end 102 | end 103 | end 104 | end 105 | 106 | describe '#run_build_image' do 107 | let(:argv) { %w[ docker/demo ] } 108 | let(:obj) { double('Chef::Knife::ContainerDockerBuild#instance')} 109 | 110 | it 'calls Chef::Knife::ContainerDockerBuild' do 111 | expect(Chef::Knife::ContainerDockerBuild).to receive(:new).with(argv).and_return(obj) 112 | expect(obj).to receive(:run) 113 | knife.run_build_image 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/chef/knife/container_docker_rebuild.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'chef/knife/container_docker_base' 19 | 20 | class Chef 21 | class Knife 22 | class ContainerDockerRebuild < Knife 23 | include Knife::ContainerDockerBase 24 | 25 | banner 'knife container docker rebuild REPO/NAME [options]' 26 | 27 | option :run_berks, 28 | long: '--[no-]berks', 29 | description: 'Run Berkshelf', 30 | default: true, 31 | boolean: true 32 | 33 | option :berks_config, 34 | long: '--berks-config CONFIG', 35 | description: 'Use the specified Berkshelf configuration' 36 | 37 | option :cleanup, 38 | long: '--[no-]cleanup', 39 | description: 'Cleanup Chef and Docker artifacts', 40 | default: true, 41 | boolean: true 42 | 43 | option :secure_dir, 44 | long: '--secure-dir DIR', 45 | description: 'Path to a local repository that contains Chef credentials.' 46 | 47 | option :force_build, 48 | long: '--force', 49 | description: 'Force the Docker image build', 50 | boolean: true 51 | 52 | option :dockerfiles_path, 53 | short: '-d PATH', 54 | long: '--dockerfiles-path PATH', 55 | description: 'Path to the directory where Docker contexts are kept', 56 | proc: proc { |d| Chef::Config[:knife][:dockerfiles_path] = d } 57 | 58 | 59 | 60 | # 61 | # Run the plugin 62 | # 63 | def run 64 | validate 65 | setup_config_defaults 66 | redownload_docker_image 67 | run_build_image 68 | end 69 | 70 | # 71 | # Reads the input parameters and validates them. 72 | # Will exit if it encounters an error 73 | # 74 | def validate 75 | if @name_args.length < 1 76 | show_usage 77 | ui.fatal('You must specify a Dockerfile name') 78 | exit 1 79 | end 80 | end 81 | 82 | # 83 | # Set defaults for configuration values 84 | # 85 | def setup_config_defaults 86 | Chef::Config[:knife][:dockerfiles_path] ||= File.join(Chef::Config[:chef_repo_path], "dockerfiles") 87 | config[:dockerfiles_path] = Chef::Config[:knife][:dockerfiles_path] 88 | end 89 | 90 | # 91 | # Redownload the BASE Docker Image, retag the HEAD and cleanup 92 | # 93 | def redownload_docker_image 94 | base_image_name = parse_dockerfile_for_base 95 | new_base_id = download_image(base_image_name) 96 | delete_image(@name_args[0]) 97 | tag_image(new_base_id, @name_args[0]) 98 | end 99 | 100 | # 101 | # Pull the BASE image name from the Dockerfile 102 | # 103 | def parse_dockerfile_for_base 104 | base_image = Chef::Config[:knife][:docker_image] || 'chef/ubuntu-12.04:latest' 105 | dockerfile = "#{docker_context}/Dockerfile" 106 | File.open(dockerfile).each do |line| 107 | if line =~ /\# BASE (\S+)/ 108 | base_image = line.match(/\# BASE (\S+)/)[1] 109 | end 110 | end 111 | ui.info("Rebuilding #{@name_args[0]} on top of #{base_image}") 112 | base_image 113 | end 114 | 115 | # 116 | # Run Chef::Knife::ContainerDockerBuild 117 | # 118 | # Note: @cli_arguments is a global var from Mixlib::CLI where argv is 119 | # put when parse_options is run as part of super's init. 120 | # 121 | def run_build_image 122 | build = Chef::Knife::ContainerDockerBuild.new(@cli_arguments) 123 | build.run 124 | end 125 | 126 | # 127 | # Returns the path to the Docker Context 128 | # 129 | # @return [String] 130 | # 131 | def docker_context 132 | File.join(config[:dockerfiles_path], dockerfile_name) 133 | end 134 | 135 | # 136 | # @return [String] the encoded name of the dockerfile 137 | # 138 | def dockerfile_name 139 | parse_dockerfile_name(@name_args[0]) 140 | end 141 | 142 | # 143 | # Returns the path to the chef-repo inside the Docker Context 144 | # 145 | # @return [String] 146 | # 147 | def chef_repo 148 | File.join(docker_context, 'chef') 149 | end 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/knife-container/skeletons/knife_container/recipes/docker_init.rb: -------------------------------------------------------------------------------- 1 | context = KnifeContainer::Generator.context 2 | dockerfile_dir = File.join(context.dockerfiles_path, context.dockerfile_name) 3 | temp_chef_repo = File.join(dockerfile_dir, 'chef') 4 | user_chef_repo = File.join(context.dockerfiles_path, '..') 5 | 6 | ## 7 | # Initial Setup 8 | # 9 | 10 | # Create Dockerfile directory (REPO/NAME) 11 | directory dockerfile_dir do 12 | recursive true 13 | end 14 | 15 | # Dockerfile 16 | template File.join(dockerfile_dir, 'Dockerfile') do 17 | source 'dockerfile.erb' 18 | helpers(KnifeContainer::Generator::TemplateHelper) 19 | end 20 | 21 | # .dockerfile 22 | template File.join(dockerfile_dir, '.dockerignore') do 23 | source 'dockerignore.erb' 24 | helpers(KnifeContainer::Generator::TemplateHelper) 25 | end 26 | 27 | # .gitignore 28 | template File.join(dockerfile_dir, '.gitignore') do 29 | source 'gitignore.erb' 30 | helpers(KnifeContainer::Generator::TemplateHelper) 31 | end 32 | 33 | 34 | ## 35 | # Initial Chef Setup 36 | # 37 | 38 | # create temp chef-repo 39 | directory temp_chef_repo do 40 | recursive true 41 | end 42 | 43 | # Client Config 44 | template File.join(temp_chef_repo, "#{context.chef_client_mode}.rb") do 45 | source 'config.rb.erb' 46 | helpers(KnifeContainer::Generator::TemplateHelper) 47 | end 48 | 49 | # First Boot JSON 50 | file File.join(temp_chef_repo, 'first-boot.json') do 51 | content context.first_boot 52 | end 53 | 54 | # Node Name 55 | template File.join(temp_chef_repo, '.node_name') do 56 | source 'node_name.erb' 57 | helpers(KnifeContainer::Generator::TemplateHelper) 58 | end 59 | 60 | ## 61 | # Resolve run list 62 | # 63 | require 'chef/run_list/run_list_item' 64 | run_list_items = context.run_list.map { |i| Chef::RunList::RunListItem.new(i) } 65 | cookbooks = [] 66 | 67 | run_list_items.each do |item| 68 | # Extract cookbook name from recipe 69 | if item.recipe? 70 | rmatch = item.name.match(/(.+?)::(.+)/) 71 | if rmatch 72 | cookbooks << rmatch[1] 73 | else 74 | cookbooks << item.name 75 | end 76 | end 77 | end 78 | 79 | # Generate Berksfile from runlist 80 | template File.join(dockerfile_dir, 'Berksfile') do 81 | source 'berksfile.erb' 82 | variables({:cookbooks => cookbooks.uniq, :berksfile_source => context.berksfile_source}) 83 | helpers(KnifeContainer::Generator::TemplateHelper) 84 | only_if { context.generate_berksfile } 85 | end 86 | 87 | # Copy over the necessary directories into the temp chef-repo (if local-mode) 88 | if context.chef_client_mode == 'zero' 89 | 90 | # generate a cookbooks directory unless we are building from a Berksfile 91 | unless context.generate_berksfile 92 | directory "#{temp_chef_repo}/cookbooks" 93 | end 94 | 95 | # Copy over cookbooks that are mentioned in the runlist. There is a gap here 96 | # that dependent cookbooks are not copied. This is a result of not having a 97 | # depsolver in the chef-client. The solution here is to use the Berkshelf integration. 98 | if context.cookbook_path.kind_of?(Array) 99 | context.cookbook_path.each do |dir| 100 | if File.exist?(File.expand_path(dir)) 101 | cookbooks.each do |cookbook| 102 | if File.exist?("#{File.expand_path(dir)}/#{cookbook}") 103 | execute "cp -rf #{File.expand_path(dir)}/#{cookbook} #{temp_chef_repo}/cookbooks/" 104 | end 105 | end 106 | else 107 | log "Could not find a '#{File.expand_path(dir)}' directory in your chef-repo." 108 | end 109 | end 110 | elsif File.exist?(File.expand_path(context.cookbook_path)) 111 | cookbooks.each do |cookbook| 112 | if File.exist?("#{File.expand_path(context.cookbook_path)}/#{cookbook}") 113 | execute "cp -rf #{File.expand_path(context.cookbook_path)}/#{cookbook} #{temp_chef_repo}/cookbooks/" 114 | end 115 | end 116 | else 117 | log "Could not find a '#{File.expand_path(context.cookbook_path)}' directory in your chef-repo." 118 | end 119 | 120 | # Because they have a smaller footprint, we will copy over all the roles, environments 121 | # and nodes. This behavior will likely change in a future version of knife-container. 122 | %w(role environment node data_bag).each do |dir| 123 | path = context.send(:"#{dir}_path") 124 | if path.kind_of?(Array) 125 | path.each do |p| 126 | execute "cp -r #{File.expand_path(p)}/ #{File.join(temp_chef_repo, "#{dir}s")}" do 127 | not_if { Dir["#{p}/*"].empty? } 128 | end 129 | end 130 | elsif path.kind_of?(String) 131 | execute "cp -r #{path}/ #{File.join(temp_chef_repo, "#{dir}s/")}" do 132 | not_if { Dir["#{path}/*"].empty? } 133 | end 134 | end 135 | end 136 | end 137 | 138 | ## 139 | # Server Only Stuff 140 | # 141 | if context.chef_client_mode == 'client' 142 | 143 | directory File.join(temp_chef_repo, 'secure') 144 | 145 | # Add validation.pem 146 | file File.join(temp_chef_repo, 'secure', 'validation.pem') do 147 | content File.read(context.validation_key) 148 | mode '0600' 149 | end 150 | 151 | # Copy over trusted certs 152 | unless Dir["#{context.trusted_certs_dir}/*"].empty? 153 | directory File.join(temp_chef_repo, 'secure', 'trusted_certs') 154 | execute "cp -r #{context.trusted_certs_dir}/* #{File.join(temp_chef_repo, 'secure', 'trusted_certs/')}" 155 | end 156 | 157 | # Copy over encrypted_data_bag_key 158 | unless context.encrypted_data_bag_secret.nil? 159 | if File.exist?(context.encrypted_data_bag_secret) 160 | file File.join(temp_chef_repo, 'secure', 'encrypted_data_bag_secret') do 161 | content File.read(context.encrypted_data_bag_secret) 162 | mode '0600' 163 | end 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Knife-Container 2 | 3 | We are glad you want to contribute to Knife-Container! 4 | 5 | We utilize **Github Issues** for issue tracking and contributions. You can contribute in two ways: 6 | 7 | 1. Reporting an issue or making a feature request [here](#issues). 8 | 2. Adding features or fixing bugs yourself and contributing your code to Chef. 9 | 10 | ## Contribution Process 11 | 12 | We have a 3 step process that utilizes **Github Issues**: 13 | 14 | 1. Sign our 15 | [Individual Contributor License Agreement (CLA)](http://supermarket.getchef.com/icla-signatures) 16 | or [Corporate CLA](http://supermarket.getchef.com/ccla-signatures) online once. 17 | 2. Create a Github Pull Request. 18 | 3. Do [Code Review](#cr) with the **Chef Engineering Team** or **ChefInit Core Committers** on the pull request. 19 | 20 | ### Chef Pull Requests 21 | 22 | Chef is built to last. We strive to ensure high quality throughout the Chef experience. In order to ensure 23 | this, we require a couple of things for all pull requests to Chef: 24 | 25 | 1. **Tests:** To ensure high quality code and protect against future regressions, we require all the 26 | code in Chef to have at least unit test coverage. See the [spec/unit](https://github.com/opscode/knife-container/tree/master/spec/unit) 27 | directory for the existing tests and use ```bundle exec rake spec``` to run them. 28 | 2. **Green Travis Run:** We use [Travis CI](https://travis-ci.org/) in order to run our tests 29 | continuously on all the pull requests. We require the Travis runs to succeed on every pull 30 | request before being merged. 31 | 32 | In addition to this it would be nice to include the description of the problem you are solving 33 | with your change. You can use [Chef Issue Template](#issuetemplate) in the description section 34 | of the pull request. 35 | 36 | ### Chef Code Review Process 37 | 38 | The Chef Code Review process happens on Github pull requests. See 39 | [this article](https://help.github.com/articles/using-pull-requests) if you're not 40 | familiar with Github Pull Requests. 41 | 42 | Once you a pull request, the **Chef Engineering Team** or **ChefInit Core Committers** will review your code 43 | and respond to you with any feedback they might have. The process at this point is as follows: 44 | 45 | 1. 2 thumbs-ups are required from the **Chef Engineering Team** or **ChefInit Core Committers** for all merges. 46 | 2. When ready, your pull request will be tagged with label `Ready For Merge`. 47 | 3. Your patch will be merged into `master` including necessary documentation updates 48 | and you will be included in `CHANGELOG.md`. Our goal is to have patches merged in 2 weeks 49 | after they are marked to be merged. 50 | 51 | If you would like to learn about when your code will be available in a release of Chef, read more about 52 | [Chef Release Process](#release). 53 | 54 | ### Contributor License Agreement (CLA) 55 | Licensing is very important to open source projects. It helps ensure the 56 | software continues to be available under the terms that the author desired. 57 | 58 | Chef uses [the Apache 2.0 license](https://github.com/opscode/chef/blob/master/LICENSE) 59 | to strike a balance between open contribution and allowing you to use the 60 | software however you would like to. 61 | 62 | The license tells you what rights you have that are provided by the copyright holder. 63 | It is important that the contributor fully understands what rights they are 64 | licensing and agrees to them. Sometimes the copyright holder isn't the contributor, 65 | most often when the contributor is doing work for a company. 66 | 67 | To make a good faith effort to ensure these criteria are met, Chef requires an Individual CLA 68 | or a Corporate CLA for contributions. This agreement helps ensure you are aware of the 69 | terms of the license you are contributing your copyrighted works under, which helps to 70 | prevent the inclusion of works in the projects that the contributor does not hold the rights 71 | to share. 72 | 73 | It only takes a few minutes to complete a CLA, and you retain the copyright to your contribution. 74 | 75 | You can complete our 76 | [Individual CLA](https://secure.echosign.com/public/hostedForm?formid=PJIF5694K6L) online. 77 | If you're contributing on behalf of your employer and they retain the copyright for your works, 78 | have your employer fill out our 79 | [Corporate CLA](https://secure.echosign.com/public/hostedForm?formid=PIE6C7AX856) instead. 80 | 81 | ### Chef Obvious Fix Policy 82 | 83 | Small contributions such as fixing spelling errors, where the content is small enough 84 | to not be considered intellectual property, can be submitted by a contributor as a patch, 85 | without a CLA. 86 | 87 | As a rule of thumb, changes are obvious fixes if they do not introduce any new functionality 88 | or creative thinking. As long as the change does not affect functionality, some likely 89 | examples include the following: 90 | 91 | * Spelling / grammar fixes 92 | * Typo correction, white space and formatting changes 93 | * Comment clean up 94 | * Bug fixes that change default return values or error codes stored in constants 95 | * Adding logging messages or debugging output 96 | * Changes to ‘metadata’ files like Gemfile, .gitignore, build scripts, etc. 97 | * Moving source files from one directory or package to another 98 | 99 | **Whenever you invoke the “obvious fix” rule, please say so in your commit message:** 100 | 101 | ``` 102 | ------------------------------------------------------------------------ 103 | commit 370adb3f82d55d912b0cf9c1d1e99b132a8ed3b5 104 | Author: danielsdeleo 105 | Date: Wed Sep 18 11:44:40 2013 -0700 106 | 107 | Fix typo in config file docs. 108 | 109 | Obvious fix. 110 | 111 | ------------------------------------------------------------------------ 112 | ``` 113 | 114 | ## Chef Issue Tracking 115 | 116 | Chef Issue Tracking is handled using Github Issues. 117 | 118 | If you are familiar with Chef and know the component that is causing you a problem or if you 119 | have a feature request on a specific component you can file an issue in the corresponding 120 | Github project. All of our Open Source Software can be found in our 121 | [Github organization](https://github.com/opscode/). 122 | 123 | Otherwise you can file your issue in the [ChefInit project](https://github.com/opscode/chef-init/issues) 124 | and we will make sure it gets filed against the appropriate project. 125 | 126 | In order to decrease the back and forth an issues and help us get to the bottom of them quickly 127 | we use below issue template. You can copy paste this code into the issue you are opening and 128 | edit it accordingly. 129 | 130 | 131 | ``` 132 | ### Version: 133 | [Version of the project installed] 134 | 135 | ### Environment: [Details about the environment such as the Operating System, cookbook details, etc...] 136 | 137 | ### Scenario: 138 | [What you are trying to achieve and you can't?] 139 | 140 | 141 | 142 | ### Steps to Reproduce: 143 | [If you are filing an issue what are the things we need to do in order to repro your problem?] 144 | 145 | 146 | ### Expected Result: 147 | [What are you expecting to happen as the consequence of above reproduction steps?] 148 | 149 | 150 | ### Actual Result: 151 | [What actually happens after the reproduction steps?] 152 | ``` 153 | -------------------------------------------------------------------------------- /lib/chef/knife/container_docker_init.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'chef/knife/container_docker_base' 19 | 20 | class Chef 21 | class Knife 22 | class ContainerDockerInit < Knife 23 | include Knife::ContainerDockerBase 24 | 25 | banner 'knife container docker init REPOSITORY/IMAGE_NAME [options]' 26 | 27 | option :base_image, 28 | short: '-f [REPOSITORY/]IMAGE[:TAG]', 29 | long: '--from [REPOSITORY/]IMAGE[:TAG]', 30 | description: 'The image to use as the base for your Docker image', 31 | proc: proc { |f| Chef::Config[:knife][:docker_image] = f } 32 | 33 | option :run_list, 34 | short: '-r RunlistItem,RunlistItem...,', 35 | long: '--run-list RUN_LIST', 36 | description: 'Comma seperated list of roles/recipes to apply to your Docker image', 37 | proc: proc { |o| o.split(/[\s,]+/) } 38 | 39 | option :local_mode, 40 | boolean: true, 41 | short: '-z', 42 | long: '--local-mode', 43 | description: 'Include and use a local chef repository to build the Docker image' 44 | 45 | option :generate_berksfile, 46 | short: '-b', 47 | long: '--berksfile', 48 | description: 'Generate a Berksfile based on the run_list provided', 49 | boolean: true, 50 | default: false 51 | 52 | option :include_credentials, 53 | long: '--include-credentials', 54 | description: 'Include secure credentials in your Docker image', 55 | boolean: true, 56 | default: false 57 | 58 | option :validation_key, 59 | long: '--validation-key PATH', 60 | description: 'The path to the validation key used by the client, typically a file named validation.pem' 61 | 62 | option :validation_client_name, 63 | long: '--validation-client-name NAME', 64 | description: 'The name of the validation client, typically a client named chef-validator' 65 | 66 | option :trusted_certs_dir, 67 | long: '--trusted-certs PATH', 68 | description: 'The path to the directory containing trusted certs' 69 | 70 | option :encrypted_data_bag_secret, 71 | long: '--secret-file SECRET_FILE', 72 | description: 'A file containing the secret key to use to encrypt data bag item values' 73 | 74 | option :chef_server_url, 75 | long: '--server-url URL', 76 | description: 'Chef Server URL' 77 | 78 | option :force, 79 | long: '--force', 80 | boolean: true, 81 | description: 'Will overwrite existing Docker Contexts' 82 | 83 | option :cookbook_path, 84 | long: '--cookbook-path PATH[:PATH]', 85 | description: 'A colon-seperated path to look for cookbooks', 86 | proc: proc { |o| o.split(':') } 87 | 88 | option :role_path, 89 | long: '--role-path PATH[:PATH]', 90 | description: 'A colon-seperated path to look for roles', 91 | proc: proc { |o| o.split(':') } 92 | 93 | option :node_path, 94 | long: '--node-path PATH[:PATH]', 95 | description: 'A colon-seperated path to look for node objects', 96 | proc: proc { |o| o.split(':') } 97 | 98 | option :environment_path, 99 | long: '--environment-path PATH[:PATH]', 100 | description: 'A colon-seperated path to look for environments', 101 | proc: proc { |o| o.split(':') } 102 | 103 | option :data_bag_path, 104 | long: '--data-bag-path PATH[:PATH]', 105 | description: 'A colon-seperated path to look for data bags', 106 | proc: proc { |o| o.split(':') } 107 | 108 | option :dockerfiles_path, 109 | short: '-d PATH', 110 | long: '--dockerfiles-path PATH', 111 | description: 'Path to the directory where Docker contexts are kept' 112 | 113 | # 114 | # Run the plugin 115 | # 116 | def run 117 | set_config_defaults 118 | validate 119 | setup_context 120 | chef_runner.converge 121 | download_and_tag_base_image 122 | ui.info("\n#{ui.color("Context Created: #{config[:dockerfiles_path]}/#{dockerfile_name}", :magenta)}") 123 | end 124 | 125 | # 126 | # Validate parameters and existing system state 127 | # 128 | def validate 129 | if @name_args.length < 1 130 | show_usage 131 | ui.fatal('You must specify a Dockerfile name') 132 | exit 1 133 | end 134 | 135 | unless valid_dockerfile_name?(@name_args[0]) 136 | show_usage 137 | ui.fatal('Your Dockerfile name cannot include a protocol or a tag.') 138 | exit 1 139 | end 140 | 141 | if config[:generate_berksfile] 142 | unless berks_installed? 143 | show_usage 144 | ui.fatal('You must have berkshelf installed to use the Berksfile flag.') 145 | exit 1 146 | end 147 | end 148 | 149 | # Check to see if the Docker context already exists. 150 | if File.exist?(File.join(config[:dockerfiles_path], dockerfile_name)) 151 | if config[:force] 152 | FileUtils.rm_rf(File.join(config[:dockerfiles_path], dockerfile_name)) 153 | else 154 | show_usage 155 | ui.fatal('The Docker Context you are trying to create already exists. ' \ 156 | 'Please use the --force flag if you would like to re-create this context.') 157 | exit 1 158 | end 159 | end 160 | end 161 | 162 | # 163 | # Set default configuration values 164 | # 165 | def set_config_defaults 166 | %w( 167 | chef_server_url 168 | cookbook_path 169 | node_path 170 | role_path 171 | environment_path 172 | data_bag_path 173 | validation_key 174 | validation_client_name 175 | trusted_certs_dir 176 | encrypted_data_bag_secret 177 | ).each do |var| 178 | config[:"#{var}"] ||= Chef::Config[:"#{var}"] 179 | end 180 | 181 | config[:base_image] ||= Chef::Config[:knife][:docker_image] || 'chef/ubuntu-12.04:latest' 182 | 183 | config[:berksfile_source] ||= Chef::Config[:knife][:berksfile_source] || 'https://supermarket.getchef.com' 184 | 185 | # if no tag is specified, use latest 186 | unless config[:base_image] =~ /[a-zA-Z0-9\/]+:[a-zA-Z0-9.\-]+/ 187 | config[:base_image] = "#{config[:base_image]}:latest" 188 | end 189 | 190 | config[:run_list] ||= [] 191 | 192 | config[:dockerfiles_path] ||= Chef::Config[:knife][:dockerfiles_path] || File.join(Chef::Config[:chef_repo_path], 'dockerfiles') 193 | 194 | config 195 | end 196 | 197 | # 198 | # Setup the generator context 199 | # 200 | def setup_context 201 | generator_context.dockerfile_name = dockerfile_name 202 | generator_context.dockerfiles_path = config[:dockerfiles_path] 203 | generator_context.base_image = config[:base_image] 204 | generator_context.chef_client_mode = chef_client_mode 205 | generator_context.run_list = config[:run_list] 206 | generator_context.cookbook_path = config[:cookbook_path] 207 | generator_context.role_path = config[:role_path] 208 | generator_context.node_path = config[:node_path] 209 | generator_context.data_bag_path = config[:data_bag_path] 210 | generator_context.environment_path = config[:environment_path] 211 | generator_context.chef_server_url = config[:chef_server_url] 212 | generator_context.validation_key = config[:validation_key] 213 | generator_context.validation_client_name = config[:validation_client_name] 214 | generator_context.trusted_certs_dir = config[:trusted_certs_dir] 215 | generator_context.encrypted_data_bag_secret = config[:encrypted_data_bag_secret] 216 | generator_context.first_boot = first_boot_content 217 | generator_context.generate_berksfile = config[:generate_berksfile] 218 | generator_context.berksfile_source = config[:berksfile_source] 219 | generator_context.include_credentials = config[:include_credentials] 220 | end 221 | 222 | # 223 | # The name of the recipe to use 224 | # 225 | # @return [String] 226 | # 227 | def recipe 228 | 'docker_init' 229 | end 230 | 231 | # 232 | # @return [String] the encoded name of the dockerfile 233 | def dockerfile_name 234 | parse_dockerfile_name(@name_args[0]) 235 | end 236 | 237 | # 238 | # Generate the JSON object for our first-boot.json 239 | # 240 | # @return [String] 241 | # 242 | def first_boot_content 243 | first_boot = {} 244 | first_boot['run_list'] = config[:run_list] 245 | Chef::JSONCompat.to_json_pretty(first_boot) 246 | end 247 | 248 | # 249 | # Return the mode in which to run: zero or client 250 | # 251 | # @return [String] 252 | # 253 | def chef_client_mode 254 | config[:local_mode] ? 'zero' : 'client' 255 | end 256 | 257 | # 258 | # Download the base Docker image and tag it with the image name 259 | # 260 | def download_and_tag_base_image 261 | ui.info("Downloading base image: #{config[:base_image]}. This process may take awhile...") 262 | image_id = download_image(config[:base_image]) 263 | image_name = config[:base_image].split(':')[0] 264 | ui.info("Tagging base image #{image_name} as #{@name_args[0]}") 265 | tag_image(image_id, @name_args[0]) 266 | end 267 | 268 | 269 | end 270 | end 271 | end 272 | -------------------------------------------------------------------------------- /lib/chef/knife/container_docker_build.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'chef/knife/container_docker_base' 19 | 20 | class Chef 21 | class Knife 22 | class ContainerDockerBuild < Knife 23 | include Knife::ContainerDockerBase 24 | 25 | deps do 26 | # These two are needed for cleanup 27 | require 'chef/node' 28 | require 'chef/api_client' 29 | end 30 | 31 | banner 'knife container docker build REPO/NAME [options]' 32 | 33 | option :run_berks, 34 | long: '--[no-]berks', 35 | description: 'Run Berkshelf', 36 | default: true, 37 | boolean: true 38 | 39 | option :berks_config, 40 | long: '--berks-config CONFIG', 41 | description: 'Use the specified Berkshelf configuration' 42 | 43 | option :cleanup, 44 | long: '--[no-]cleanup', 45 | description: 'Cleanup Chef and Docker artifacts', 46 | default: true, 47 | boolean: true 48 | 49 | option :secure_dir, 50 | long: '--secure-dir DIR', 51 | description: 'Path to a local repository that contains Chef credentials.' 52 | 53 | option :force_build, 54 | long: '--force', 55 | description: 'Force the Docker image build', 56 | boolean: true 57 | 58 | option :tags, 59 | long: '--tags TAG[,TAG]', 60 | description: 'Comma separated list of tags you wish to apply to the image.', 61 | default: ['latest'], 62 | proc: proc { |o| o.split(/[\s,]+/) } 63 | 64 | option :dockerfiles_path, 65 | short: '-d PATH', 66 | long: '--dockerfiles-path PATH', 67 | description: 'Path to the directory where Docker contexts are kept', 68 | proc: proc { |d| Chef::Config[:knife][:dockerfiles_path] = d } 69 | 70 | # 71 | # Run the plugin 72 | # 73 | def run 74 | validate 75 | setup_config_defaults 76 | run_berks if config[:run_berks] 77 | backup_secure unless config[:secure_dir].nil? 78 | build_docker_image 79 | restore_secure unless config[:secure_dir].nil? 80 | cleanup_artifacts if config[:cleanup] 81 | end 82 | 83 | # 84 | # Reads the input parameters and validates them. 85 | # Will exit if it encounters an error 86 | # 87 | def validate 88 | if @name_args.length < 1 89 | show_usage 90 | ui.fatal('You must specify a Dockerfile name') 91 | exit 1 92 | end 93 | 94 | unless valid_dockerfile_name?(@name_args[0]) 95 | show_usage 96 | ui.fatal('Your Dockerfile name cannot include a protocol or a tag.') 97 | exit 1 98 | end 99 | 100 | # if secure_dir doesn't exist or is missing files, exit 101 | if config[:secure_dir] 102 | case 103 | when !File.directory?(config[:secure_dir]) 104 | ui.fatal("SECURE_DIRECTORY: The directory #{config[:secure_dir]}" \ 105 | " does not exist.") 106 | exit 1 107 | when !File.exist?(File.join(config[:secure_dir], 'validation.pem')) && 108 | !File.exist?(File.join(config[:secure_dir], 'client.pem')) 109 | ui.fatal("SECURE_DIRECTORY: Can not find validation.pem or client.pem" \ 110 | " in #{config[:secure_dir]}.") 111 | exit 1 112 | end 113 | end 114 | 115 | # if berkshelf isn't installed, set run_berks to false 116 | unless berks_installed? 117 | ui.warn('The berks executable could not be found. Resolving the Berksfile will be skipped.') 118 | config[:run_berks] = false 119 | end 120 | 121 | if config[:berks_config] 122 | unless File.exist?(config[:berks_config]) 123 | ui.fatal("No Berksfile configuration found at #{config[:berks_config]}") 124 | exit 1 125 | end 126 | end 127 | end 128 | 129 | # 130 | # Set defaults for configuration values 131 | # 132 | def setup_config_defaults 133 | Chef::Config[:knife][:dockerfiles_path] ||= File.join(Chef::Config[:chef_repo_path], 'dockerfiles') 134 | config[:dockerfiles_path] = Chef::Config[:knife][:dockerfiles_path] 135 | 136 | # Determine if we are running local or server mode 137 | case 138 | when File.exist?(File.join(config[:dockerfiles_path], dockerfile_name, 'chef', 'zero.rb')) 139 | config[:local_mode] = true 140 | when File.exist?(File.join(config[:dockerfiles_path], dockerfile_name, 'chef', 'client.rb')) 141 | config[:local_mode] = false 142 | else 143 | show_usage 144 | ui.fatal("Can not find a Chef configuration file in #{config[:dockerfiles_path]}/#{dockerfile_name}/chef") 145 | exit 1 146 | end 147 | end 148 | 149 | # 150 | # Execute berkshelf locally 151 | # 152 | def run_berks 153 | if File.exist?(File.join(docker_context, 'Berksfile')) 154 | if File.exist?(File.join(chef_repo, 'zero.rb')) 155 | run_berks_vendor 156 | elsif File.exist?(File.join(chef_repo, 'client.rb')) 157 | run_berks_upload 158 | end 159 | end 160 | end 161 | 162 | # 163 | # Determines whether a Berksfile exists in the Docker context 164 | # 165 | # @returns [TrueClass, FalseClass] 166 | # 167 | def berksfile_exists? 168 | File.exist?(File.join(docker_context, 'Berksfile')) 169 | end 170 | 171 | 172 | # 173 | # Installs all the cookbooks via Berkshelf 174 | # 175 | def run_berks_install 176 | run_command('berks install') 177 | end 178 | 179 | # 180 | # Vendors all the cookbooks into a directory inside the Docker Context 181 | # 182 | def run_berks_vendor 183 | if File.exist?(File.join(chef_repo, 'cookbooks')) 184 | if config[:force_build] 185 | FileUtils.rm_rf(File.join(chef_repo, 'cookbooks')) 186 | else 187 | show_usage 188 | ui.fatal('A `cookbooks` directory already exists. You must either remove this directory from your dockerfile directory or use the `force` flag') 189 | exit 1 190 | end 191 | end 192 | 193 | run_berks_install 194 | run_command("berks vendor #{chef_repo}/cookbooks") 195 | end 196 | 197 | # 198 | # Upload the cookbooks to the Chef Server 199 | # 200 | def run_berks_upload 201 | run_berks_install 202 | berks_upload_cmd = 'berks upload' 203 | berks_upload_cmd << ' --force' if config[:force_build] 204 | berks_upload_cmd << " --config=#{File.expand_path(config[:berks_config])}" if config[:berks_config] 205 | run_command(berks_upload_cmd) 206 | end 207 | 208 | # 209 | # Builds the Docker image 210 | # 211 | def build_docker_image 212 | image = build_image(docker_context) 213 | 214 | # Apply the name and tags 215 | # By default, it will apply the latest tag but this behavior can be 216 | # overwritten by excluding the latest tag from the specified list. 217 | config[:tags].each do |tag| 218 | tag_image(image.id, @name_args[0], tag) 219 | end 220 | end 221 | 222 | # 223 | # Move `secure` folder to `backup_secure` and copy the config[:secure_dir] 224 | # to the new `secure` folder. 225 | # 226 | # Note: The .dockerignore file has a line to ignore backup secure to it 227 | # should not be included in the image. 228 | # 229 | def backup_secure 230 | FileUtils.mv("#{docker_context}/chef/secure", 231 | "#{docker_context}/chef/secure_backup") 232 | FileUtils.cp_r(config[:secure_dir], "#{docker_context}/chef/secure") 233 | end 234 | 235 | # 236 | # Delete the temporary secure directory and restore the original from the 237 | # backup. 238 | # 239 | def restore_secure 240 | FileUtils.rm_rf("#{docker_context}/chef/secure") 241 | FileUtils.mv("#{docker_context}/chef/secure_backup", 242 | "#{docker_context}/chef/secure") 243 | end 244 | 245 | # 246 | # Cleanup build artifacts 247 | # 248 | def cleanup_artifacts 249 | unless config[:local_mode] 250 | destroy_item(Chef::Node, node_name, 'node') 251 | destroy_item(Chef::ApiClient, node_name, 'client') 252 | end 253 | end 254 | 255 | # 256 | # Run a shell command from the Docker Context directory 257 | # 258 | def run_command(cmd) 259 | Open3.popen2e(cmd, chdir: docker_context) do |stdin, stdout_err, wait_thr| 260 | while line = stdout_err.gets 261 | puts line 262 | end 263 | wait_thr.value.to_i 264 | end 265 | end 266 | 267 | # 268 | # Returns the path to the Docker Context 269 | # 270 | # @return [String] 271 | # 272 | def docker_context 273 | File.join(config[:dockerfiles_path], dockerfile_name) 274 | end 275 | 276 | # 277 | # Returns the encoded Dockerfile name 278 | # 279 | def dockerfile_name 280 | parse_dockerfile_name(@name_args[0]) 281 | end 282 | 283 | # 284 | # Returns the path to the chef-repo inside the Docker Context 285 | # 286 | # @return [String] 287 | # 288 | def chef_repo 289 | File.join(docker_context, 'chef') 290 | end 291 | 292 | # 293 | # Generates a node name for the Docker container 294 | # 295 | # @return [String] 296 | # 297 | def node_name 298 | File.read(File.join(chef_repo, '.node_name')).strip 299 | end 300 | 301 | # Extracted from Chef::Knife.delete_object, because it has a 302 | # confirmation step built in... By not specifying the '--no-cleanup' 303 | # flag the user is already making their intent known. It is not 304 | # necessary to make them confirm two more times. 305 | def destroy_item(klass, name, type_name) 306 | begin 307 | object = klass.load(name) 308 | object.destroy 309 | ui.warn("Deleted #{type_name} #{name}") 310 | rescue Net::HTTPServerException 311 | ui.warn("Could not find a #{type_name} named #{name} to delete!") 312 | end 313 | end 314 | end 315 | end 316 | end 317 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /spec/unit/container_docker_init_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'spec_helper' 19 | require 'chef/knife/container_docker_init' 20 | 21 | describe Chef::Knife::ContainerDockerInit do 22 | 23 | let(:stdout_io) { StringIO.new } 24 | let(:stderr_io) { StringIO.new } 25 | 26 | def stdout 27 | stdout_io.string 28 | end 29 | 30 | let(:default_cookbook_path) do 31 | File.expand_path('cookbooks', fixtures_path) 32 | end 33 | 34 | let(:reset_chef_config) do 35 | Chef::Config.reset 36 | Chef::Config[:chef_repo_path] = tempdir 37 | Chef::Config[:knife][:dockerfiles_path] = File.join(tempdir, 'dockerfiles') 38 | Chef::Config[:knife][:docker_image] = nil 39 | Chef::Config[:cookbook_path] = File.join(fixtures_path, 'cookbooks') 40 | Chef::Config[:chef_server_url] = "http://localhost:4000" 41 | Chef::Config[:validation_key] = File.join(fixtures_path, '.chef', 'validator.pem') 42 | Chef::Config[:trusted_certs_dir] = File.join(fixtures_path, '.chef', 'trusted_certs') 43 | Chef::Config[:validation_client_name] = 'masterchef' 44 | Chef::Config[:encrypted_data_bag_secret] = File.join(fixtures_path, '.chef', 'encrypted_data_bag_secret') 45 | end 46 | 47 | def generator_context 48 | KnifeContainer::Generator.context 49 | end 50 | 51 | subject(:knife) do 52 | Chef::Knife::ContainerDockerInit.new(argv).tap do |c| 53 | allow(c).to receive(:output).and_return(true) 54 | allow(c).to receive(:download_and_tag_base_image) 55 | allow(c.ui).to receive(:stdout).and_return(stdout_io) 56 | allow(c.chef_runner).to receive(:stdout).and_return(stdout_io) 57 | end 58 | end 59 | 60 | let(:argv) { %w[ docker/demo ] } 61 | 62 | describe '#run' do 63 | it 'initializes a new docker context' do 64 | expect(knife).to receive(:validate) 65 | expect(knife).to receive(:set_config_defaults) 66 | expect(knife).to receive(:setup_context) 67 | expect(knife.chef_runner).to receive(:converge) 68 | expect(knife).to receive(:download_and_tag_base_image) 69 | knife.run 70 | end 71 | end 72 | 73 | # 74 | # Validating parameters 75 | # 76 | describe '#validate' do 77 | context 'when no arguments are given' do 78 | let(:argv) { %w[] } 79 | 80 | it 'prints the show_usage message and exits' do 81 | expect(knife).to receive(:show_usage) 82 | expect(knife.ui).to receive(:fatal) 83 | expect{ knife.validate }.to raise_error(SystemExit) 84 | end 85 | end 86 | 87 | context 'when -b flag has been specified' do 88 | let(:argv) {%W[ docker/demo -b ]} 89 | 90 | it 'fails if berkshelf is not installed' do 91 | expect(knife).to receive(:berks_installed?).and_return(false) 92 | expect(knife.ui).to receive(:fatal) 93 | expect{ knife.validate }.to raise_error(SystemExit) 94 | end 95 | end 96 | 97 | context 'when an invalid dockerfile name is given' do 98 | let(:argv) { %w[ http://reg.example.com/demo ] } 99 | 100 | it 'throws an error' do 101 | expect(knife).to receive(:valid_dockerfile_name?).and_return(false) 102 | expect(knife.ui).to receive(:fatal) 103 | expect{ knife.validate }.to raise_error(SystemExit) 104 | end 105 | end 106 | 107 | let(:context_path) { File.join(Chef::Config[:chef_repo_path], 'dockerfiles', 'docker', 'demo') } 108 | 109 | context 'the context already exists' do 110 | before do 111 | reset_chef_config 112 | knife.config[:dockerfiles_path] = File.join(Chef::Config[:chef_repo_path], 'dockerfiles') 113 | allow(File).to receive(:exist?).with(context_path).and_return(true) 114 | end 115 | 116 | it 'warns the user when the context they are trying to create already exists' do 117 | expect(knife).to receive(:show_usage) 118 | expect(knife.ui).to receive(:fatal) 119 | expect { knife.validate }.to raise_error(SystemExit) 120 | end 121 | 122 | context 'but --force was specified' do 123 | let(:argv) { %w[ docker/demo --force ] } 124 | it 'will delete the existing folder and proceed as normal' do 125 | expect(FileUtils).to receive(:rm_rf).with(context_path).and_return(true) 126 | knife.validate 127 | end 128 | end 129 | end 130 | end 131 | 132 | # 133 | # Setting up configuration values 134 | # 135 | describe '#set_config_defaults' do 136 | before(:each) { reset_chef_config } 137 | 138 | context 'when no cli overrides have been specified' do 139 | it 'sets values to Chef::Config default values' do 140 | config = knife.set_config_defaults 141 | expect(config[:chef_server_url]).to eq('http://localhost:4000') 142 | expect(config[:cookbook_path]).to eq(File.join(fixtures_path, 'cookbooks')) 143 | expect(config[:node_path]).to eq(File.join(tempdir, 'nodes')) 144 | expect(config[:role_path]).to eq(File.join(tempdir, 'roles')) 145 | expect(config[:environment_path]).to eq(File.join(tempdir, 'environments')) 146 | expect(config[:data_bag_path]).to eq(File.join(tempdir, 'data_bags')) 147 | expect(config[:dockerfiles_path]).to eq(File.join(tempdir, 'dockerfiles')) 148 | expect(config[:run_list]).to eq([]) 149 | end 150 | 151 | context 'when cookbook_path is an array' do 152 | before do 153 | Chef::Config[:cookbook_path] = ['/path/to/cookbooks', '/path/to/site-cookbooks'] 154 | end 155 | 156 | it 'honors the array' do 157 | config = knife.set_config_defaults 158 | expect(config[:cookbook_path]).to eq(['/path/to/cookbooks', '/path/to/site-cookbooks']) 159 | end 160 | end 161 | end 162 | 163 | context 'when base image is specified' do 164 | context 'with a tag' do 165 | let(:argv) { %w[ docker/demo -f docker/demo:11.12.8 ] } 166 | 167 | it 'respects that tag' do 168 | config = knife.set_config_defaults 169 | expect(config[:base_image]).to eql('docker/demo:11.12.8') 170 | end 171 | end 172 | 173 | context 'without a tag' do 174 | let(:argv) { %w[ docker/demo -f docker/demo ] } 175 | 176 | it 'should append the \'latest\' tag on the name' do 177 | config = knife.set_config_defaults 178 | expect(config[:base_image]).to eql("docker/demo:latest") 179 | end 180 | end 181 | end 182 | end 183 | 184 | describe '#setup_context' do 185 | it 'calls helper methods to calculate more complex values' do 186 | expect(knife).to receive(:dockerfile_name) 187 | expect(knife).to receive(:chef_client_mode) 188 | expect(knife).to receive(:first_boot_content) 189 | knife.setup_context 190 | end 191 | end 192 | 193 | # 194 | # The chef runner converge 195 | # 196 | describe '#converge' do 197 | 198 | context 'when -b is passed' do 199 | before(:each) { reset_chef_config } 200 | let(:argv) { %W[ 201 | docker/demo 202 | -r role[demo],recipe[demo::recipe],recipe[nginx] 203 | -b 204 | ]} 205 | 206 | subject(:berksfile) do 207 | File.read("#{tempdir}/dockerfiles/docker/demo/Berksfile") 208 | end 209 | 210 | it 'generates a Berksfile based on the run_list' do 211 | knife.run 212 | expect(berksfile).to match(/cookbook "nginx"/) 213 | expect(berksfile).to match(/cookbook "demo"/) 214 | end 215 | end 216 | 217 | context 'when -z is passed' do 218 | before { reset_chef_config } 219 | let(:argv) { %w[ docker/demo -z ] } 220 | 221 | let(:expected_container_file_relpaths) do 222 | %w[ 223 | Dockerfile 224 | .dockerignore 225 | chef/first-boot.json 226 | chef/zero.rb 227 | chef/.node_name 228 | ] 229 | end 230 | 231 | let(:expected_files) do 232 | expected_container_file_relpaths.map do |relpath| 233 | File.join(Chef::Config[:chef_repo_path], 'dockerfiles', 'docker/demo', relpath) 234 | end 235 | end 236 | 237 | it 'creates a folder to manage the Dockerfile and Chef files' do 238 | knife.run 239 | generated_files = Dir.glob("#{Chef::Config[:chef_repo_path]}/dockerfiles/docker/demo/**{,/*/**}/*", File::FNM_DOTMATCH) 240 | expected_files.each do |expected_file| 241 | expect(generated_files).to include(expected_file) 242 | end 243 | end 244 | 245 | it 'creates config with local-mode specific values' do 246 | knife.run 247 | config_file = File.read("#{tempdir}/dockerfiles/docker/demo/chef/zero.rb") 248 | expect(config_file).to include "require 'chef-init'" 249 | expect(config_file).to include 'node_name', 'ChefInit.node_name' 250 | expect(config_file).to include 'ssl_verify_mode', ':verify_peer' 251 | expect(config_file).to include 'cookbook_path', '["/etc/chef/cookbooks"]' 252 | expect(config_file).to include 'encrypted_data_bag_secret', '/etc/chef/secure/encrypted_data_bag_secret' 253 | end 254 | 255 | describe 'copies cookbooks to temporary chef-repo' do 256 | let(:argv) { %W[docker/demo -r recipe[nginx],recipe[apt] -z ]} 257 | before do 258 | reset_chef_config 259 | Chef::Config[:cookbook_path] = ["#{fixtures_path}/cookbooks", "#{fixtures_path}/site-cookbooks"] 260 | knife.run 261 | end 262 | 263 | it 'copies cookbooks from both directories' do 264 | expect(stdout).to include("execute[cp -rf #{fixtures_path}/cookbooks/nginx #{tempdir}/dockerfiles/docker/demo/chef/cookbooks/] action run") 265 | expect(stdout).to include("execute[cp -rf #{fixtures_path}/site-cookbooks/apt #{tempdir}/dockerfiles/docker/demo/chef/cookbooks/] action run") 266 | end 267 | 268 | it 'only copies cookbooks that exist in the run_list' do 269 | expect(stdout).not_to include("execute[cp -rf #{default_cookbook_path}/dummy #{Chef::Config[:chef_repo_path]}/dockerfiles/docker/demo/chef/cookbooks/] action run") 270 | end 271 | end 272 | 273 | describe 'when invalid cookbook path is specified' do 274 | let(:argv) { %W[ docker/demo -r recipe[nginx] -z]} 275 | 276 | before(:each) do 277 | reset_chef_config 278 | Chef::Config[:cookbook_path] = '/tmp/nil/cookbooks' 279 | end 280 | 281 | it 'logs an error and does not copy cookbooks' do 282 | knife.run 283 | expect(stdout).to include('log[Could not find a \'/tmp/nil/cookbooks\' directory in your chef-repo.] action write') 284 | end 285 | end 286 | end 287 | 288 | describe 'chef/client.rb' do 289 | before { reset_chef_config } 290 | subject(:config_file) do 291 | File.read("#{tempdir}/dockerfiles/docker/demo/chef/client.rb") 292 | end 293 | 294 | it 'creates config with server specific configuration' do 295 | knife.run 296 | expect(config_file).to match(/require 'chef-init'/) 297 | expect(config_file).to include 'node_name', 'ChefInit.node_name' 298 | expect(config_file).to include 'ssl_verify_mode', ':verify_peer' 299 | expect(config_file).to include 'chef_server_url', 'http://localhost:4000' 300 | expect(config_file).to include 'validation_key', '/etc/chef/secure/validation.pem' 301 | expect(config_file).to include 'client_key', '/etc/chef/secure/client.pem' 302 | expect(config_file).to include 'trusted_certs_dir', '/etc/chef/secure/trusted_certs' 303 | expect(config_file).to include 'validation_client_name', 'masterchef' 304 | expect(config_file).to include 'encrypted_data_bag_secret', '/etc/chef/secure/encrypted_data_bag_secret' 305 | end 306 | 307 | context 'when encrypted_data_bag_secret is not specified' do 308 | before do 309 | reset_chef_config 310 | Chef::Config[:encrypted_data_bag_secret] = nil 311 | end 312 | 313 | it 'is not present in config' do 314 | knife.run 315 | expect(config_file).to_not include 'encrypted_data_bag_secret' 316 | end 317 | end 318 | end 319 | 320 | describe 'Dockerfile' do 321 | let(:argv) { %w[ docker/demo --include-credentials ] } 322 | subject(:dockerfile) do 323 | File.read("#{Chef::Config[:chef_repo_path]}/dockerfiles/docker/demo/Dockerfile") 324 | end 325 | 326 | before do 327 | reset_chef_config 328 | knife.run 329 | end 330 | 331 | it 'sets the base_image name in a comment in the Dockerfile' do 332 | expect(dockerfile).to include '# BASE chef/ubuntu-12.04:latest' 333 | end 334 | 335 | it 'does not remove the secure directory' do 336 | expect(dockerfile).to include 'RUN chef-init --bootstrap --no-remove-secure' 337 | end 338 | end 339 | 340 | let(:expected_container_file_relpaths) do 341 | %w[ 342 | Dockerfile 343 | .dockerignore 344 | chef/first-boot.json 345 | chef/client.rb 346 | chef/secure/validation.pem 347 | chef/secure/trusted_certs/chef_example_com.crt 348 | chef/.node_name 349 | ] 350 | end 351 | 352 | let(:expected_files) do 353 | expected_container_file_relpaths.map do |relpath| 354 | File.join(Chef::Config[:chef_repo_path], 'dockerfiles', 'docker/demo', relpath) 355 | end 356 | end 357 | 358 | it 'creates a folder to manage the Dockerfile and Chef files' do 359 | knife.run 360 | generated_files = Dir.glob("#{Chef::Config[:chef_repo_path]}/dockerfiles/docker/demo/**{,/*/**}/*", File::FNM_DOTMATCH) 361 | expected_files.each do |expected_file| 362 | expect(generated_files).to include(expected_file) 363 | end 364 | end 365 | end 366 | end 367 | -------------------------------------------------------------------------------- /spec/unit/container_docker_build_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright:: Copyright (c) 2014 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 'spec_helper' 19 | require 'chef/knife/container_docker_build' 20 | Chef::Knife::ContainerDockerBuild.load_deps 21 | 22 | describe Chef::Knife::ContainerDockerBuild do 23 | 24 | let(:stdout_io) { StringIO.new } 25 | let(:stderr_io) { StringIO.new } 26 | let(:argv) { %w[ docker/demo ] } 27 | 28 | def stdout 29 | stdout_io.string 30 | end 31 | 32 | let(:default_dockerfiles_path) do 33 | File.expand_path("dockerfiles", fixtures_path) 34 | end 35 | 36 | subject(:knife) do 37 | Chef::Knife::ContainerDockerBuild.new(argv).tap do |c| 38 | allow(c).to receive(:output).and_return(true) 39 | c.parse_options(argv) 40 | c.merge_configs 41 | end 42 | end 43 | 44 | describe '#run' do 45 | before(:each) do 46 | allow(knife).to receive(:run_berks) 47 | allow(knife).to receive(:build_docker_image) 48 | allow(knife).to receive(:cleanup_artifacts) 49 | allow(knife).to receive(:berks_installed?).and_return(true) 50 | Chef::Config.reset 51 | Chef::Config[:chef_repo_path] = tempdir 52 | allow(File).to receive(:exist?).with(File.join(tempdir, 'dockerfiles', 'docker', 'demo', 'chef', 'zero.rb')).and_return(true) 53 | end 54 | 55 | context 'by default' do 56 | let(:argv) { %w[ docker/demo ] } 57 | 58 | it 'parses argv, run berkshelf, build the image and cleanup the artifacts' do 59 | expect(knife).to receive(:validate).and_call_original 60 | expect(knife).to receive(:setup_config_defaults).and_call_original 61 | expect(knife).to receive(:run_berks) 62 | expect(knife).to receive(:build_docker_image) 63 | expect(knife).to receive(:cleanup_artifacts) 64 | knife.run 65 | end 66 | end 67 | 68 | context '--no-berks is passed' do 69 | let(:argv) { %w[ docker/demo --no-berks ] } 70 | 71 | it 'does not run berkshelf' do 72 | expect(knife).not_to receive(:run_berks) 73 | knife.run 74 | end 75 | end 76 | 77 | context '--no-cleanup is passed' do 78 | let(:argv) { %w[ docker/demo --no-cleanup ] } 79 | 80 | it 'does not clean up the artifacts' do 81 | expect(knife).not_to receive(:cleanup_artifacts) 82 | knife.run 83 | end 84 | end 85 | 86 | context 'when --secure-dir is passed' do 87 | let(:argv) { %w[ docker/demo --secure-dir /path/to/dir ] } 88 | 89 | before do 90 | allow(File).to receive(:directory?).with('/path/to/dir').and_return(true) 91 | allow(File).to receive(:exist?).with('/path/to/dir/validation.pem').and_return(true) 92 | allow(File).to receive(:exist?).with('/path/to/dir/client.pem').and_return(false) 93 | end 94 | 95 | it 'uses contents of specified directory for secure credentials during build' do 96 | expect(knife).to receive(:backup_secure) 97 | expect(knife).to receive(:restore_secure) 98 | knife.run 99 | end 100 | end 101 | end 102 | 103 | describe '#validate' do 104 | let(:argv) { %W[] } 105 | 106 | before { allow(knife).to receive(:berks_installed?).and_return(true) } 107 | 108 | context 'when argv is empty' do 109 | it 'prints usage and exits' do 110 | expect(knife).to receive(:show_usage) 111 | expect(knife.ui).to receive(:fatal) 112 | expect { knife.run }.to raise_error(SystemExit) 113 | end 114 | end 115 | 116 | context 'when Berkshelf is not installed' do 117 | let(:argv) { %w[ docker/demo ] } 118 | 119 | before { allow(knife).to receive(:berks_installed?).and_return(false) } 120 | 121 | it 'does not run berks' do 122 | expect(knife.ui).to receive(:warn) 123 | knife.validate 124 | expect(knife.config[:run_berks]).to eql(false) 125 | end 126 | end 127 | 128 | context 'when --no-cleanup was passed' do 129 | let(:argv) { %w[ docker/demo --no-cleanup ] } 130 | 131 | it 'sets config[:cleanup] to false' do 132 | knife.validate 133 | expect(knife.config[:cleanup]).to eql(false) 134 | end 135 | end 136 | 137 | context 'when --no-berks was not passed' do 138 | let(:argv) { %w[ docker/demo ] } 139 | 140 | context 'and Berkshelf is not installed' do 141 | let(:berks_output) { double("berks -v output", stdout: "berks not found") } 142 | 143 | before do 144 | allow(knife).to receive(:berks_installed?).and_return(false) 145 | end 146 | 147 | it 'sets run_berks to false' do 148 | knife.validate 149 | expect(knife.config[:run_berks]).to eql(false) 150 | end 151 | end 152 | end 153 | 154 | context 'when --berks-config was passed' do 155 | let(:argv) { %w[ docker/demo --berks-config my_berkshelf/config.json ] } 156 | 157 | context 'and configuration file does not exist' do 158 | before do 159 | allow(File).to receive(:exist?).with('my_berkshelf/config.json').and_return(false) 160 | end 161 | 162 | it 'exits immediately' do 163 | expect(knife.ui).to receive(:fatal) 164 | expect { knife.validate }.to raise_error(SystemExit) 165 | end 166 | end 167 | end 168 | 169 | context 'when --secure-dir is passed' do 170 | let(:argv) { %w[ docker/demo --secure-dir /path/to/dir ] } 171 | 172 | context 'and directory does not exist' do 173 | before { allow(File).to receive(:directory?).with('/path/to/dir').and_return(false) } 174 | 175 | it 'throws an error' do 176 | expect(knife.ui).to receive(:fatal) 177 | expect { knife.validate }.to raise_error(SystemExit) 178 | end 179 | end 180 | 181 | context 'and validation or client key does not exist' do 182 | before do 183 | allow(File).to receive(:directory?).with('/path/to/dir').and_return(false) 184 | allow(File).to receive(:exist?).with('/path/to/dir/validation.pem').and_return(false) 185 | allow(File).to receive(:exist?).with('/path/to/dir/client.pem').and_return(false) 186 | end 187 | 188 | it 'throws an error' do 189 | expect(knife.ui).to receive(:fatal) 190 | expect { knife.validate }.to raise_error(SystemExit) 191 | end 192 | end 193 | end 194 | 195 | context 'when an invalid dockerfile name is given' do 196 | let(:argv) { %w[ http://reg.example.com/demo ] } 197 | 198 | it 'throws an error' do 199 | expect(knife).to receive(:valid_dockerfile_name?).and_return(false) 200 | expect(knife.ui).to receive(:fatal) 201 | expect{ knife.validate }.to raise_error(SystemExit) 202 | end 203 | end 204 | end 205 | 206 | describe '#setup_config_defaults' do 207 | before do 208 | Chef::Config.reset 209 | Chef::Config[:chef_repo_path] = tempdir 210 | allow(File).to receive(:exist?).with(File.join(tempdir, 'dockerfiles', 'docker', 'demo', 'chef', 'zero.rb')).and_return(true) 211 | end 212 | 213 | let(:argv) { %w[ docker/demo ]} 214 | 215 | context 'Chef::Config[:dockerfiles_path] has not been set' do 216 | it 'sets dockerfiles_path to Chef::Config[:chef_repo_path]/dockerfiles' do 217 | allow($stdout).to receive(:write) 218 | knife.setup_config_defaults 219 | expect(knife.config[:dockerfiles_path]).to eql("#{Chef::Config[:chef_repo_path]}/dockerfiles") 220 | end 221 | end 222 | end 223 | 224 | describe "#run_berks" do 225 | let(:argv) { %W[ docker/demo ] } 226 | 227 | before(:each) do 228 | Chef::Config.reset 229 | Chef::Config[:chef_repo_path] = tempdir 230 | Chef::Config[:knife][:dockerfiles_path] = default_dockerfiles_path 231 | end 232 | 233 | let(:docker_context) { File.join(Chef::Config[:knife][:dockerfiles_path], 'docker', 'demo') } 234 | 235 | context 'when there is no Berksfile' do 236 | before { allow(File).to receive(:exist?).with(File.join(docker_context, 'Berksfile')).and_return(false) } 237 | 238 | it 'returns doing nothing' do 239 | expect(knife).not_to receive(:run_berks_vendor) 240 | expect(knife).not_to receive(:run_berks_upload) 241 | knife.run_berks 242 | end 243 | end 244 | 245 | context 'when docker image was init in local mode' do 246 | before do 247 | allow(File).to receive(:exist?).with(File.join(docker_context, 'Berksfile')).and_return(true) 248 | allow(File).to receive(:exist?).with(File.join(docker_context, 'chef', 'zero.rb')).and_return(true) 249 | allow(File).to receive(:exist?).with(File.join(docker_context, 'chef', 'client.rb')).and_return(false) 250 | allow(knife).to receive(:chef_repo).and_return(File.join(docker_context, "chef")) 251 | end 252 | 253 | it 'calls run_berks_vendor' do 254 | expect(knife).to receive(:run_berks_vendor) 255 | knife.run_berks 256 | end 257 | end 258 | 259 | context 'when docker image was init in client mode' do 260 | before do 261 | allow(File).to receive(:exist?).with(File.join(docker_context, 'Berksfile')).and_return(true) 262 | allow(File).to receive(:exist?).with(File.join(docker_context, 'chef', 'zero.rb')).and_return(false) 263 | allow(File).to receive(:exist?).with(File.join(docker_context, 'chef', 'client.rb')).and_return(true) 264 | allow(knife).to receive(:chef_repo).and_return(File.join(docker_context, "chef")) 265 | end 266 | 267 | it 'calls run_berks_upload' do 268 | expect(knife).to receive(:run_berks_upload) 269 | knife.run_berks 270 | end 271 | end 272 | end 273 | 274 | describe '#run_berks_install' do 275 | it 'calls `berks install`' do 276 | expect(knife).to receive(:run_command).with('berks install') 277 | knife.run_berks_install 278 | end 279 | end 280 | 281 | describe '#run_berks_vendor' do 282 | 283 | before(:each) do 284 | Chef::Config.reset 285 | Chef::Config[:chef_repo_path] = tempdir 286 | Chef::Config[:knife][:dockerfiles_path] = default_dockerfiles_path 287 | allow(knife).to receive(:docker_context).and_return(File.join(default_dockerfiles_path, 'docker', 'demo')) 288 | allow(knife).to receive(:run_berks_install) 289 | end 290 | 291 | let(:docker_context) { File.join(Chef::Config[:knife][:dockerfiles_path], 'docker', 'demo') } 292 | 293 | context "cookbooks directory already exists in docker context" do 294 | before do 295 | allow(File).to receive(:exist?).with(File.join(docker_context, 'chef', 'cookbooks')).and_return(true) 296 | end 297 | 298 | context 'and force-build was specified' do 299 | let(:argv) { %w[ docker/demo --force ]} 300 | 301 | it "deletes the existing cookbooks directory and runs berks.vendor" do 302 | expect(FileUtils).to receive(:rm_rf).with(File.join(docker_context, 'chef', 'cookbooks')) 303 | expect(knife).to receive(:run_berks_install) 304 | expect(knife).to receive(:run_command).with("berks vendor #{File.join(docker_context, 'chef', 'cookbooks')}") 305 | knife.run_berks_vendor 306 | end 307 | 308 | end 309 | 310 | context 'and force-build was not specified' do 311 | let(:argv) { %w[ docker-demo ] } 312 | 313 | it 'errors out' do 314 | allow($stdout).to receive(:write) 315 | allow($stderr).to receive(:write) 316 | expect { knife.run_berks_vendor }.to raise_error(SystemExit) 317 | end 318 | end 319 | end 320 | 321 | context 'cookbooks directory does not yet exist' do 322 | before do 323 | allow(File).to receive(:exist?).with(File.join(docker_context, 'chef', 'cookbooks')).and_return(false) 324 | end 325 | 326 | it 'calls berks.vendor' do 327 | expect(knife).to receive(:run_berks_install) 328 | expect(knife).to receive(:run_command).with("berks vendor #{File.join(docker_context, 'chef', 'cookbooks')}") 329 | knife.run_berks_vendor 330 | end 331 | end 332 | end 333 | 334 | describe '#run_berks_upload' do 335 | before(:each) do 336 | Chef::Config.reset 337 | Chef::Config[:chef_repo_path] = tempdir 338 | Chef::Config[:knife][:dockerfiles_path] = default_dockerfiles_path 339 | allow(knife).to receive(:docker_context).and_return(File.join(default_dockerfiles_path, 'docker', 'demo')) 340 | allow(knife).to receive(:run_berks_install) 341 | end 342 | 343 | let(:docker_context) { File.join(Chef::Config[:knife][:dockerfiles_path], 'docker', 'local') } 344 | 345 | context 'by default' do 346 | before do 347 | knife.config[:force_build] = false 348 | end 349 | 350 | it 'should call berks install' do 351 | allow(knife).to receive(:run_command).with('berks upload') 352 | expect(knife).to receive(:run_berks_install) 353 | knife.run_berks_upload 354 | end 355 | 356 | it 'should run berks upload' do 357 | expect(knife).to receive(:run_command).with('berks upload') 358 | knife.run_berks_upload 359 | end 360 | end 361 | 362 | context 'when force-build is specified' do 363 | before do 364 | knife.config[:force_build] = true 365 | end 366 | 367 | it 'should run berks upload with force' do 368 | expect(knife).to receive(:run_command).with('berks upload --force') 369 | knife.run_berks_upload 370 | end 371 | end 372 | 373 | context 'when berks-config is specified' do 374 | before do 375 | knife.config[:berks_config] = 'my_berkshelf/config.json' 376 | allow(File).to receive(:exist?).with('my_berkshelf/config.json').and_return(true) 377 | allow(File).to receive(:expand_path).with('my_berkshelf/config.json').and_return('/home/my_berkshelf/config.json') 378 | end 379 | 380 | it 'should run berks upload with specified config file' do 381 | expect(knife).to receive(:run_command).with('berks upload --config=/home/my_berkshelf/config.json') 382 | knife.run_berks_upload 383 | end 384 | end 385 | 386 | context 'when berks-config _and_ force-build is specified' do 387 | before do 388 | knife.config[:force_build] = true 389 | knife.config[:berks_config] = 'my_berkshelf/config.json' 390 | allow(File).to receive(:exist?).with('my_berkshelf/config.json').and_return(true) 391 | allow(File).to receive(:expand_path).with('my_berkshelf/config.json').and_return('/home/my_berkshelf/config.json') 392 | end 393 | 394 | it 'should run berks upload with specified config file _and_ force flag' do 395 | expect(knife).to receive(:run_command).with('berks upload --force --config=/home/my_berkshelf/config.json') 396 | knife.run_berks_upload 397 | end 398 | end 399 | end 400 | 401 | describe '#dockerfile_name' do 402 | it 'encodes the dockerfile name' do 403 | expect(knife).to receive(:parse_dockerfile_name) 404 | knife.dockerfile_name 405 | end 406 | end 407 | 408 | describe '#cleanup_artifacts' do 409 | let(:argv) { %w[ docker/demo ] } 410 | before { allow(knife).to receive(:node_name).and_return('docker-demo-build') } 411 | 412 | context 'running in server-mode' do 413 | it 'should delete the node and client objects from the Chef Server' do 414 | expect(knife).to receive(:destroy_item).with(Chef::Node, 'docker-demo-build', 'node') 415 | expect(knife).to receive(:destroy_item).with(Chef::ApiClient, 'docker-demo-build', 'client') 416 | knife.cleanup_artifacts 417 | end 418 | end 419 | end 420 | end 421 | --------------------------------------------------------------------------------