knife-container 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +4 -0
  5. data/CONTRIBUTING.md +152 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE +201 -0
  8. data/README.md +59 -0
  9. data/Rakefile +16 -0
  10. data/knife-container.gemspec +31 -0
  11. data/lib/chef/knife/container_docker_build.rb +243 -0
  12. data/lib/chef/knife/container_docker_init.rb +262 -0
  13. data/lib/knife-container/chef_runner.rb +83 -0
  14. data/lib/knife-container/command.rb +45 -0
  15. data/lib/knife-container/generator.rb +88 -0
  16. data/lib/knife-container/helpers.rb +16 -0
  17. data/lib/knife-container/skeletons/knife_container/files/default/plugins/docker_container.rb +37 -0
  18. data/lib/knife-container/skeletons/knife_container/metadata.rb +7 -0
  19. data/lib/knife-container/skeletons/knife_container/recipes/docker_init.rb +181 -0
  20. data/lib/knife-container/skeletons/knife_container/templates/default/berksfile.erb +5 -0
  21. data/lib/knife-container/skeletons/knife_container/templates/default/config.rb.erb +16 -0
  22. data/lib/knife-container/skeletons/knife_container/templates/default/dockerfile.erb +9 -0
  23. data/lib/knife-container/skeletons/knife_container/templates/default/dockerignore.erb +0 -0
  24. data/lib/knife-container/skeletons/knife_container/templates/default/node_name.erb +1 -0
  25. data/lib/knife-container/version.rb +5 -0
  26. data/spec/functional/docker_container_ohai_spec.rb +20 -0
  27. data/spec/functional/fixtures/ohai/Dockerfile +3 -0
  28. data/spec/spec_helper.rb +35 -0
  29. data/spec/test_helpers.rb +59 -0
  30. data/spec/unit/container_docker_build_spec.rb +325 -0
  31. data/spec/unit/container_docker_init_spec.rb +464 -0
  32. data/spec/unit/fixtures/.chef/encrypted_data_bag_secret +0 -0
  33. data/spec/unit/fixtures/.chef/trusted_certs/chef_example_com.crt +0 -0
  34. data/spec/unit/fixtures/.chef/validator.pem +1 -0
  35. data/spec/unit/fixtures/Berksfile +3 -0
  36. data/spec/unit/fixtures/cookbooks/dummy/metadata.rb +0 -0
  37. data/spec/unit/fixtures/cookbooks/nginx/metadata.rb +0 -0
  38. data/spec/unit/fixtures/environments/dev.json +0 -0
  39. data/spec/unit/fixtures/nodes/demo.json +0 -0
  40. data/spec/unit/fixtures/roles/base.json +0 -0
  41. data/spec/unit/fixtures/site-cookbooks/apt/metadata.rb +0 -0
  42. metadata +232 -0
@@ -0,0 +1,16 @@
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 progress')
8
+ end.join(' ')
9
+ end
10
+
11
+ desc 'Run all tests'
12
+ task :test => [:spec]
13
+
14
+
15
+ task :default => [:test]
16
+
@@ -0,0 +1,31 @@
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.add_dependency "chef", "~> 11.0"
22
+ spec.add_dependency "mixlib-config", "~> 2.0"
23
+ spec.add_dependency "json", ">= 1.4.4", "<= 1.8.1"
24
+
25
+ spec.add_development_dependency 'rspec', '~> 2.14'
26
+ spec.add_development_dependency 'simplecov', '~> 0.7.1'
27
+ spec.add_development_dependency 'bundler', '~> 1.3'
28
+ spec.add_development_dependency 'rake'
29
+ spec.add_development_dependency 'pry'
30
+ spec.add_development_dependency 'docker-api', '~> 1.11.1'
31
+ end
@@ -0,0 +1,243 @@
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 'chef/mixin/shell_out'
20
+
21
+ class Chef
22
+ class Knife
23
+ class ContainerDockerBuild < Knife
24
+ include Chef::Mixin::ShellOut
25
+
26
+ deps do
27
+ # These two are needed for cleanup
28
+ require 'chef/node'
29
+ require 'chef/api_client'
30
+ end
31
+
32
+ banner "knife container docker build REPO/NAME [options]"
33
+
34
+ option :run_berks,
35
+ :long => "--[no-]berks",
36
+ :description => "Run Berkshelf",
37
+ :default => true,
38
+ :boolean => true
39
+
40
+ option :cleanup,
41
+ :long => "--[no-]cleanup",
42
+ :description => "Cleanup Chef and Docker artifacts",
43
+ :default => true,
44
+ :boolean => true
45
+
46
+ option :force_build,
47
+ :long => "--force",
48
+ :description => "Force the Docker image build",
49
+ :boolean => true
50
+
51
+ option :dockerfiles_path,
52
+ :short => "-d PATH",
53
+ :long => "--dockerfiles-path PATH",
54
+ :description => "Path to the directory where Docker contexts are kept",
55
+ :proc => Proc.new { |d| Chef::Config[:knife][:dockerfiles_path] = d }
56
+
57
+ #
58
+ # Run the plugin
59
+ #
60
+ def run
61
+ read_and_validate_params
62
+ setup_config_defaults
63
+ run_berks if config[:run_berks]
64
+ build_image
65
+ cleanup_artifacts if config[:cleanup]
66
+ end
67
+
68
+ #
69
+ # Reads the input parameters and validates them.
70
+ # Will exit if it encounters an error
71
+ #
72
+ def read_and_validate_params
73
+ if @name_args.length < 1
74
+ show_usage
75
+ ui.fatal("You must specify a Dockerfile name")
76
+ exit 1
77
+ end
78
+
79
+ # if berkshelf isn't installed, set run_berks to false
80
+ if config[:run_berks]
81
+ ver = shell_out("berks -v")
82
+ config[:run_berks] = ver.stdout.match(/\d+\.\d+\.\d+/) ? true : false
83
+ end
84
+ end
85
+
86
+ #
87
+ # Set defaults for configuration values
88
+ #
89
+ def setup_config_defaults
90
+ Chef::Config[:knife][:dockerfiles_path] ||= File.join(Chef::Config[:chef_repo_path], "dockerfiles")
91
+ config[:dockerfiles_path] = Chef::Config[:knife][:dockerfiles_path]
92
+
93
+ # Determine if we are running local or server mode
94
+ case
95
+ when File.exists?(File.join(config[:dockerfiles_path], @name_args[0], 'chef', 'zero.rb'))
96
+ config[:local_mode] = true
97
+ when File.exists?(File.join(config[:dockerfiles_path], @name_args[0], 'chef', 'client.rb'))
98
+ config[:local_mode] = false
99
+ else
100
+ show_usage
101
+ ui.fatal("Can not find a Chef configuration file in #{config[:dockerfiles_path]}/#{@name_args[0]}/chef")
102
+ exit 1
103
+ end
104
+ end
105
+
106
+ #
107
+ # Execute berkshelf locally
108
+ #
109
+ def run_berks
110
+ if File.exists?(File.join(docker_context, "Berksfile"))
111
+ if File.exists?(File.join(chef_repo, "zero.rb"))
112
+ run_berks_vendor
113
+ elsif File.exists?(File.join(chef_repo, "client.rb"))
114
+ run_berks_upload
115
+ end
116
+ end
117
+ end
118
+
119
+ #
120
+ # Determines whether a Berksfile exists in the Docker context
121
+ #
122
+ # @returns [TrueClass, FalseClass]
123
+ #
124
+ def berksfile_exists?
125
+ File.exists?(File.join(docker_context, "Berksfile"))
126
+ end
127
+
128
+ #
129
+ # Installs all the cookbooks via Berkshelf
130
+ #
131
+ def run_berks_install
132
+ run_command("berks install")
133
+ end
134
+
135
+ #
136
+ # Vendors all the cookbooks into a directory inside the Docker Context
137
+ #
138
+ def run_berks_vendor
139
+ if File.exists?(File.join(chef_repo, "cookbooks"))
140
+ if config[:force_build]
141
+ FileUtils.rm_rf(File.join(chef_repo, "cookbooks"))
142
+ else
143
+ show_usage
144
+ ui.fatal("A `cookbooks` directory already exists. You must either remove this directory from your dockerfile directory or use the `force` flag")
145
+ exit 1
146
+ end
147
+ end
148
+
149
+ run_berks_install
150
+ run_command("berks vendor #{chef_repo}/cookbooks")
151
+ end
152
+
153
+ #
154
+ # Upload the cookbooks to the Chef Server
155
+ #
156
+ def run_berks_upload
157
+ run_berks_install
158
+ if config[:force_build]
159
+ run_command("berks upload --force")
160
+ else
161
+ run_command("berks upload")
162
+ end
163
+ end
164
+
165
+ #
166
+ # Builds the Docker image
167
+ #
168
+ def build_image
169
+ run_command(docker_build_command)
170
+ end
171
+
172
+ #
173
+ # Cleanup build artifacts
174
+ #
175
+ def cleanup_artifacts
176
+ unless config[:local_mode]
177
+ destroy_item(Chef::Node, node_name, "node")
178
+ destroy_item(Chef::ApiClient, node_name, "client")
179
+ end
180
+ end
181
+
182
+ #
183
+ # The command to use to build the Docker image
184
+ #
185
+ def docker_build_command
186
+ "docker build -t #{@name_args[0]} #{docker_context}"
187
+ end
188
+
189
+ #
190
+ # Run a shell command from the Docker Context directory
191
+ #
192
+ def run_command(cmd)
193
+ Open3.popen2e(cmd, chdir: docker_context) do |stdin, stdout_err, wait_thr|
194
+ while line = stdout_err.gets
195
+ puts line
196
+ end
197
+ wait_thr.value.to_i
198
+ end
199
+ end
200
+
201
+ #
202
+ # Returns the path to the Docker Context
203
+ #
204
+ # @return [String]
205
+ #
206
+ def docker_context
207
+ File.join(config[:dockerfiles_path], @name_args[0])
208
+ end
209
+
210
+ #
211
+ # Returns the path to the chef-repo inside the Docker Context
212
+ #
213
+ # @return [String]
214
+ #
215
+ def chef_repo
216
+ File.join(docker_context, "chef")
217
+ end
218
+
219
+ #
220
+ # Generates a node name for the Docker container
221
+ #
222
+ # @return [String]
223
+ #
224
+ def node_name
225
+ "#{@name_args[0].gsub('/','-')}-build"
226
+ end
227
+
228
+ # Extracted from Chef::Knife.delete_object, because it has a
229
+ # confirmation step built in... By not specifying the '--no-cleanup'
230
+ # flag the user is already making their intent known. It is not
231
+ # necessary to make them confirm two more times.
232
+ def destroy_item(klass, name, type_name)
233
+ begin
234
+ object = klass.load(name)
235
+ object.destroy
236
+ ui.warn("Deleted #{type_name} #{name}")
237
+ rescue Net::HTTPServerException
238
+ ui.warn("Could not find a #{type_name} named #{name} to delete!")
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,262 @@
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 'json'
19
+ require 'chef/knife'
20
+ require 'knife-container/command'
21
+ require 'chef/mixin/shell_out'
22
+
23
+ class Chef
24
+ class Knife
25
+ class ContainerDockerInit < Knife
26
+
27
+ include KnifeContainer::Command
28
+ include Chef::Mixin::ShellOut
29
+
30
+ banner "knife container docker init REPO/NAME [options]"
31
+
32
+ option :base_image,
33
+ :short => "-f [REPO/]IMAGE[:TAG]",
34
+ :long => "--from [REPO/]IMAGE[:TAG]",
35
+ :description => "The image to use for the FROM value in your Dockerfile",
36
+ :proc => Proc.new { |f| Chef::Config[:knife][:docker_image] = f }
37
+
38
+ option :run_list,
39
+ :short => "-r RunlistItem,RunlistItem...,",
40
+ :long => "--run-list RUN_LIST",
41
+ :description => "Comma seperated list of roles/recipes to apply to your Docker image",
42
+ :proc => Proc.new { |o| o.split(/[\s,]+/) }
43
+
44
+ option :local_mode,
45
+ :boolean => true,
46
+ :short => "-z",
47
+ :long => "--local-mode",
48
+ :description => "Include and use a local chef repository to build the Docker image"
49
+
50
+ option :generate_berksfile,
51
+ :short => "-b",
52
+ :long => "--berksfile",
53
+ :description => "Generate a Berksfile based on the run_list provided",
54
+ :boolean => true,
55
+ :default => false
56
+
57
+ option :include_credentials,
58
+ :long => "--include-credentials",
59
+ :description => "Include secure credentials in your Docker image",
60
+ :boolean => true,
61
+ :default => false
62
+
63
+ option :validation_key,
64
+ :long => "--validation-key PATH",
65
+ :description => "The path to the validation key used by the client, typically a file named validation.pem"
66
+
67
+ option :validation_client_name,
68
+ :long => "--validation-client-name NAME",
69
+ :description => "The name of the validation client, typically a client named chef-validator"
70
+
71
+ option :trusted_certs_dir,
72
+ :long => "--trusted-certs PATH",
73
+ :description => "The path to the directory containing trusted certs"
74
+
75
+ option :encrypted_data_bag_secret,
76
+ :long => "--secret-file SECRET_FILE",
77
+ :description => "A file containing the secret key to use to encrypt data bag item values"
78
+
79
+ option :chef_server_url,
80
+ :long => "--server-url URL",
81
+ :description => "Chef Server URL"
82
+
83
+ option :force,
84
+ :long => "--force",
85
+ :boolean => true,
86
+ :desription => "Will overwrite existing Docker Contexts"
87
+
88
+ option :cookbook_path,
89
+ :long => "--cookbook-path PATH[:PATH]",
90
+ :description => "A colon-seperated path to look for cookbooks",
91
+ :proc => Proc.new { |o| o.split(':') }
92
+
93
+ option :role_path,
94
+ :long => "--role-path PATH[:PATH]",
95
+ :description => "A colon-seperated path to look for roles",
96
+ :proc => Proc.new { |o| o.split(':') }
97
+
98
+ option :node_path,
99
+ :long => "--node-path PATH[:PATH]",
100
+ :description => "A colon-seperated path to look for node objects",
101
+ :proc => Proc.new { |o| o.split(':') }
102
+
103
+ option :environment_path,
104
+ :long => "--environment-path PATH[:PATH]",
105
+ :description => "A colon-seperated path to look for environments",
106
+ :proc => Proc.new { |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
+ :proc => Proc.new { |d| Chef::Config[:knife][:dockerfiles_path] = d }
113
+
114
+ #
115
+ # Run the plugin
116
+ #
117
+ def run
118
+ read_and_validate_params
119
+ set_config_defaults
120
+ eval_current_system
121
+ setup_context
122
+ chef_runner.converge
123
+ download_and_tag_base_image
124
+ ui.info("\n#{ui.color("Context Created: #{config[:dockerfiles_path]}/#{@name_args[0]}", :magenta)}")
125
+ end
126
+
127
+ #
128
+ # Read and validate the parameters
129
+ #
130
+ def read_and_validate_params
131
+ if @name_args.length < 1
132
+ show_usage
133
+ ui.fatal("You must specify a Dockerfile name")
134
+ exit 1
135
+ end
136
+
137
+ if config[:generate_berksfile]
138
+ begin
139
+ require 'berkshelf'
140
+ rescue LoadError
141
+ show_usage
142
+ ui.fatal("You must have the Berkshelf gem installed to use the Berksfile flag.")
143
+ exit 1
144
+ end
145
+ end
146
+ end
147
+
148
+ #
149
+ # Set default configuration values
150
+ # We do this here and not in the option syntax because the Chef::Config
151
+ # is not available to us at that point. It also gives us a space to set
152
+ # other defaults.
153
+ #
154
+ def set_config_defaults
155
+ %w(
156
+ chef_server_url
157
+ cookbook_path
158
+ node_path
159
+ role_path
160
+ environment_path
161
+ validation_key
162
+ validation_client_name
163
+ trusted_certs_dir
164
+ encrypted_data_bag_secret
165
+ ).each do |var|
166
+ config[:"#{var}"] ||= Chef::Config[:"#{var}"]
167
+ end
168
+
169
+ config[:base_image] ||= "chef/ubuntu-12.04:latest"
170
+
171
+ # if no tag is specified, use latest
172
+ unless config[:base_image] =~ /[a-zA-Z0-9\/]+:[a-zA-Z0-9.\-]+/
173
+ config[:base_image] = "#{config[:base_image]}:latest"
174
+ end
175
+
176
+ config[:run_list] ||= []
177
+
178
+ Chef::Config[:knife][:dockerfiles_path] ||= File.join(Chef::Config[:chef_repo_path], "dockerfiles")
179
+ config[:dockerfiles_path] = Chef::Config[:knife][:dockerfiles_path]
180
+ end
181
+
182
+ #
183
+ # Setup the generator context
184
+ #
185
+ def setup_context
186
+ generator_context.dockerfile_name = @name_args[0]
187
+ generator_context.dockerfiles_path = config[:dockerfiles_path]
188
+ generator_context.base_image = config[:base_image]
189
+ generator_context.chef_client_mode = chef_client_mode
190
+ generator_context.run_list = config[:run_list]
191
+ generator_context.cookbook_path = config[:cookbook_path]
192
+ generator_context.role_path = config[:role_path]
193
+ generator_context.node_path = config[:node_path]
194
+ generator_context.environment_path = config[:environment_path]
195
+ generator_context.chef_server_url = config[:chef_server_url]
196
+ generator_context.validation_key = config[:validation_key]
197
+ generator_context.validation_client_name = config[:validation_client_name]
198
+ generator_context.trusted_certs_dir = config[:trusted_certs_dir]
199
+ generator_context.encrypted_data_bag_secret = config[:encrypted_data_bag_secret]
200
+ generator_context.first_boot = first_boot_content
201
+ generator_context.generate_berksfile = config[:generate_berksfile]
202
+ generator_context.include_credentials = config[:include_credentials]
203
+ end
204
+
205
+ #
206
+ # The name of the recipe to use
207
+ #
208
+ # @return [String]
209
+ #
210
+ def recipe
211
+ "docker_init"
212
+ end
213
+
214
+ #
215
+ # Generate the JSON object for our first-boot.json
216
+ #
217
+ # @return [String]
218
+ #
219
+ def first_boot_content
220
+ first_boot = {}
221
+ first_boot['run_list'] = config[:run_list]
222
+ JSON.pretty_generate(first_boot)
223
+ end
224
+
225
+ #
226
+ # Return the mode in which to run: zero or client
227
+ #
228
+ # @return [String]
229
+ #
230
+ def chef_client_mode
231
+ config[:local_mode] ? "zero" : "client"
232
+ end
233
+
234
+ #
235
+ # Download the base Docker image and tag it with the image name
236
+ #
237
+ def download_and_tag_base_image
238
+ ui.info("Downloading base image: #{config[:base_image]}. This process may take awhile...")
239
+ shell_out("docker pull #{config[:base_image]}")
240
+ image_name = config[:base_image].split(':')[0]
241
+ ui.info("Tagging base image #{image_name} as #{@name_args[0]}")
242
+ shell_out("docker tag #{image_name} #{@name_args[0]}")
243
+ end
244
+
245
+ #
246
+ # Run some evaluations on the system to make sure it is in the state we need.
247
+ #
248
+ def eval_current_system
249
+ # Check to see if the Docker context already exists.
250
+ if File.exist?(File.join(config[:dockerfiles_path], @name_args[0]))
251
+ if config[:force]
252
+ FileUtils.rm_rf(File.join(config[:dockerfiles_path], @name_args[0]))
253
+ else
254
+ show_usage
255
+ ui.fatal("The Docker Context you are trying to create already exists. Please use the --force flag if you would like to re-create this context.")
256
+ exit 1
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end