knife-container 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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