cide 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 879d029028c2a327c2408a454a5f53d5eb62f228
4
- data.tar.gz: f08ca0dc63f6b81748ee498cd813f4c4f5e0cf57
3
+ metadata.gz: c84c09a6870e5146c1a42a86e03e1d01a2dc74aa
4
+ data.tar.gz: 4a4b246cc68b455bd26c8185c6dde9aaa1a6e8b5
5
5
  SHA512:
6
- metadata.gz: 50435c37507f74d08ad919a0dd5726d5a492ec14005748cbcd2b532e85e7b720ed524a6b8212e2eb42f2a7f98506f1aaf581a74051b4da717a59faff42ad00da
7
- data.tar.gz: 5d168e00d5dc07d2458f6fbb4b42d76d8219aef4ae6ff30b7241c4d45a542e09dd04b0b83c4cd04433a0c3ed707c374383a027761e8e8d5f40a25e46f717a1f6
6
+ metadata.gz: 8e69cb3488d0a15e18ab4031083aa3c33c720757236d28135de981e0d98ea8dbeb95807d48a236b4617492551880049129cee649a49508288b7d9e22c0662dbf
7
+ data.tar.gz: 8b0a452b1d6a159f4362dc11674576d9c6ca34c9b6c56a0b1bdab0727b64ee07a6d13dfdcc2df93838d662048cb5817292ed5951e8d95170fe58ccbe6fc51dae
data/.editorconfig ADDED
@@ -0,0 +1,13 @@
1
+ # EditorConfig is awesome: http://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ # Unix-style newlines with a newline ending every file
7
+ [*]
8
+ end_of_line = lf
9
+ insert_final_newline = true
10
+
11
+ [*.rb]
12
+ indent_style = space
13
+ indent_size = 2
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation -rspec_helper
data/.rubocop.yml CHANGED
@@ -1,33 +1,50 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'spec/**/*'
4
+
5
+ # Rubocop is not smart enough
6
+ Metrics/AbcSize:
7
+ Max: 100
8
+
1
9
  # CIDE::CLI is essentially a big dispatcher, no need to break it
2
10
  # in smaller chunks.
3
11
  Metrics/ClassLength:
4
12
  Max: 200
5
13
 
14
+ # Offense count: 28
15
+ Metrics/CyclomaticComplexity:
16
+ Max: 15
17
+
6
18
  # CIDE::CLI methods can be read top to bottom. No need to factor out
7
19
  # functionality unless it can be shared.
8
20
  Metrics/MethodLength:
9
21
  Max: 60
10
22
 
23
+ Metrics/PerceivedComplexity:
24
+ Max: 20
25
+
11
26
  # Don't align stuff vertically, bad for diffing
12
27
  Style/AlignParameters:
13
28
  EnforcedStyle: with_fixed_indentation
14
29
 
15
- # Allows for easy diffing
16
- # Keep them sorted alphabetically unless a meaningful order exists
17
- Style/TrailingComma:
18
- EnforcedStyleForMultiline: comma
30
+ # Don't obsess over missing documentation for now
31
+ Style/Documentation:
32
+ Enabled: false
19
33
 
20
- # $? is not equivalent to $CHILD_STATUS
21
- Style/SpecialGlobalVars:
34
+ # Don't agree with rubocop here
35
+ Style/MultilineOperationIndentation:
22
36
  Enabled: false
23
37
 
24
38
  # Prefering the short style
25
39
  Style/PerlBackrefs:
26
40
  Enabled: false
27
41
 
28
- # Offense count: 28
29
- Metrics/CyclomaticComplexity:
30
- Max: 15
42
+ # $? is not equivalent to $CHILD_STATUS
43
+ Style/SpecialGlobalVars:
44
+ Enabled: false
45
+
46
+ # Allows for easy diffing
47
+ # Keep them sorted alphabetically unless a meaningful order exists
48
+ Style/TrailingComma:
49
+ EnforcedStyleForMultiline: comma
31
50
 
32
- Metrics/PerceivedComplexity:
33
- Max: 20
data/CHANGELOG.md CHANGED
@@ -1,4 +1,14 @@
1
1
 
2
+ 0.2.0 / 2015-04-15
3
+ ==================
4
+
5
+ * NEW: .cide.yml schema loader
6
+ * NEW: Support for linked containers
7
+ * NEW: Much better build output
8
+ * CHANGE: Docker version: 1.5.0+ is required
9
+ * FIX: Avoid name clashes with projects who have a Dockerfile already
10
+ * FIX: cide --export
11
+
2
12
  0.1.1 / 2015-01-27
3
13
  ==================
4
14
 
data/Gemfile.lock CHANGED
@@ -2,34 +2,76 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  cide (0.1.1)
5
- thor
5
+ thor (~> 0.19.0)
6
+ virtus (~> 1.0.0)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
11
+ activesupport (4.2.1)
12
+ i18n (~> 0.7)
13
+ json (~> 1.7, >= 1.7.7)
14
+ minitest (~> 5.1)
15
+ thread_safe (~> 0.3, >= 0.3.4)
16
+ tzinfo (~> 1.1)
10
17
  ast (2.0.0)
11
18
  astrolabe (1.3.0)
12
19
  parser (>= 2.2.0.pre.3, < 3.0)
13
- parser (2.2.0.pre.4)
20
+ axiom-types (0.1.1)
21
+ descendants_tracker (~> 0.0.4)
22
+ ice_nine (~> 0.11.0)
23
+ thread_safe (~> 0.3, >= 0.3.1)
24
+ coercible (1.0.0)
25
+ descendants_tracker (~> 0.0.1)
26
+ descendants_tracker (0.0.4)
27
+ thread_safe (~> 0.3, >= 0.3.1)
28
+ diff-lcs (1.2.5)
29
+ equalizer (0.0.11)
30
+ i18n (0.7.0)
31
+ ice_nine (0.11.1)
32
+ json (1.8.1)
33
+ minitest (5.4.3)
34
+ parser (2.2.0.3)
14
35
  ast (>= 1.1, < 3.0)
15
- slop (~> 3.4, >= 3.4.5)
16
- powerpack (0.0.9)
36
+ powerpack (0.1.0)
17
37
  rainbow (2.0.0)
18
- rake (10.1.0)
19
- rubocop (0.26.1)
38
+ rake (10.4.2)
39
+ rspec (3.2.0)
40
+ rspec-core (~> 3.2.0)
41
+ rspec-expectations (~> 3.2.0)
42
+ rspec-mocks (~> 3.2.0)
43
+ rspec-core (3.2.3)
44
+ rspec-support (~> 3.2.0)
45
+ rspec-expectations (3.2.1)
46
+ diff-lcs (>= 1.2.0, < 2.0)
47
+ rspec-support (~> 3.2.0)
48
+ rspec-mocks (3.2.1)
49
+ diff-lcs (>= 1.2.0, < 2.0)
50
+ rspec-support (~> 3.2.0)
51
+ rspec-support (3.2.2)
52
+ rubocop (0.30.0)
20
53
  astrolabe (~> 1.3)
21
- parser (>= 2.2.0.pre.4, < 3.0)
22
- powerpack (~> 0.0.6)
54
+ parser (>= 2.2.0.1, < 3.0)
55
+ powerpack (~> 0.1)
23
56
  rainbow (>= 1.99.1, < 3.0)
24
57
  ruby-progressbar (~> 1.4)
25
- ruby-progressbar (1.6.0)
26
- slop (3.6.0)
58
+ ruby-progressbar (1.7.5)
27
59
  thor (0.19.1)
60
+ thread_safe (0.3.5)
61
+ tzinfo (1.2.2)
62
+ thread_safe (~> 0.1)
63
+ virtus (1.0.5)
64
+ axiom-types (~> 0.1)
65
+ coercible (~> 1.0)
66
+ descendants_tracker (~> 0.0, >= 0.0.3)
67
+ equalizer (~> 0.0, >= 0.0.9)
28
68
 
29
69
  PLATFORMS
30
70
  ruby
31
71
 
32
72
  DEPENDENCIES
73
+ activesupport
33
74
  cide!
34
75
  rake
76
+ rspec
35
77
  rubocop
data/README.md CHANGED
@@ -20,7 +20,7 @@ Example
20
20
  `.cide.yml`
21
21
  ```
22
22
  ---
23
- image: "ruby:2.1"
23
+ from: "ruby:2.1"
24
24
  as_root:
25
25
  - apt-get update -qy && apt-get install -qy libxml2-dev
26
26
  command: bundle && bundle exec rspec
@@ -32,35 +32,34 @@ Features
32
32
  * straighforward to use, just run `cide` inside of your project
33
33
  * works on OSX with boot2docker
34
34
  * integrates easily with jenkins or other CI systems
35
+ * can run with linked containers
35
36
 
36
37
  Limitations
37
38
  -----------
38
39
 
39
- A temporary Dockerfile has to be created in the project's root because docker
40
- doesn't allow referencing files outside of the directory (even with a symlink)
40
+ Docker version 1.5.0+ is required
41
41
 
42
42
  Installation
43
43
  ------------
44
44
 
45
45
  Install docker: https://docs.docker.com/installation/#installation
46
46
 
47
- ```
47
+ ```sh
48
48
  gem install cide
49
49
  ```
50
50
 
51
51
  OSX docker install:
52
- ```
52
+ ```sh
53
53
  brew install boot2docker
54
54
  boot2docker init
55
55
  boot2docker up
56
56
  # cide auto-detects boot2docker on OSX
57
57
  ```
58
58
 
59
- TODO
60
- ----
59
+ Similar projects
60
+ ----------------
61
+
62
+ * [Docker Compose](https://docs.docker.com/compose/) - Docker development environment
61
63
 
62
- * linked container
63
- * schema validation
64
- * find a way to import SSH keys
65
- * cleaner output. on error is just outputs a backtrace
64
+ Open an issue if a project is missing.
66
65
 
data/Rakefile CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'rubocop/rake_task'
2
+ require 'rspec/core/rake_task'
2
3
 
3
- task default: [:rubocop]
4
+ task default: [:spec, :rubocop]
4
5
 
5
6
  RuboCop::RakeTask.new
7
+ RSpec::Core::RakeTask.new
data/cide.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'cide'
5
- s.version = '0.1.1'
5
+ s.version = '0.2.0'
6
6
  s.authors = ['zimbatm']
7
7
  s.email = ['zimbatm@zimbatm.com']
8
8
  s.summary = 'CI docker runner'
@@ -13,14 +13,17 @@ DESC
13
13
  s.homepage = 'https://github.com/zimbatm/cide'
14
14
  s.license = 'MIT'
15
15
 
16
- s.executables << 'cide'
16
+ s.executables = ['cide']
17
17
  s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
18
18
  s.test_files = `git ls-files spec`.split($INPUT_RECORD_SEPARATOR)
19
19
  s.require_paths = ['lib']
20
20
 
21
21
  s.required_ruby_version = '>= 1.9.3'
22
22
 
23
- s.add_runtime_dependency 'thor'
23
+ s.add_runtime_dependency 'thor', '~> 0.19.0'
24
+ s.add_runtime_dependency 'virtus', '~> 1.0.0'
24
25
  s.add_development_dependency 'rake'
25
26
  s.add_development_dependency 'rubocop'
27
+ s.add_development_dependency 'rspec'
28
+ s.add_development_dependency 'activesupport'
26
29
  end
@@ -0,0 +1,84 @@
1
+ require 'virtus'
2
+
3
+ require 'erb'
4
+ require 'yaml'
5
+
6
+ require 'cide/build/config_loader'
7
+
8
+ module CIDE
9
+ module Build
10
+ class Config
11
+ DOCKERFILE_TEMPLATE =
12
+ File.expand_path('../../dockerfile_template.erb', __FILE__)
13
+
14
+ module NiceInspect
15
+ def inspect
16
+ out = []
17
+ attributes.each_pair do |key, value|
18
+ out << "#{key}=#{value.inspect}"
19
+ end
20
+ "<#{out.join(' ')}>"
21
+ end
22
+ end
23
+
24
+ class Step
25
+ include Virtus.model(strict: true)
26
+ include NiceInspect
27
+ attribute :add, Array[String], default: []
28
+ attribute :forward_env, Array[String], default: []
29
+ attribute :run, Array[String], default: []
30
+ end
31
+
32
+ class Link
33
+ include Virtus.model
34
+ include NiceInspect
35
+ attribute :name, String
36
+ attribute :image, String
37
+ attribute :env, Hash[String, String]
38
+ attribute :run, String
39
+ # Container ID added after the fact
40
+ attr_accessor :id
41
+ end
42
+
43
+ include Virtus.model(strict: true)
44
+ include NiceInspect
45
+ attribute :from, String, default: 'ubuntu'
46
+ attribute :as_root, Array[String], default: []
47
+ attribute :use_ssh, Boolean, default: false
48
+ attribute :before, Step, required: false
49
+ attribute :forward_env, Array[String], default: []
50
+ attribute :export_dir, String, required: false
51
+ attribute :links, Array[Link], default: []
52
+ attribute :run, String, default: 'script/ci'
53
+
54
+ attr_reader :warnings, :errors
55
+
56
+ def initialize(*)
57
+ super
58
+ @warnings = []
59
+ @errors = []
60
+ end
61
+
62
+ def to_dockerfile
63
+ ERB.new(File.read(DOCKERFILE_TEMPLATE), nil, '<>-').result(binding)
64
+ end
65
+
66
+ def self.load_file(file_path, output = $stderr)
67
+ obj = new
68
+ loader = ConfigLoader.new(obj)
69
+ loader.load YAML.load_file(file_path)
70
+
71
+ obj.warnings.each do |warn|
72
+ output.puts "WARN: #{warn}"
73
+ end
74
+
75
+ obj.errors.each do |error|
76
+ output.puts "ERROR: #{error}"
77
+ end
78
+
79
+ return obj if obj.errors.empty?
80
+ nil
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,227 @@
1
+ module CIDE
2
+ module Build
3
+ class ConfigLoader
4
+ class Path
5
+ attr_reader :to_s
6
+ def initialize(str)
7
+ @to_s = str.to_s
8
+ end
9
+
10
+ def append(value)
11
+ self.class.new(
12
+ @to_s + (value.is_a?(Integer) ? "[#{value}]" : ".#{value}"),
13
+ )
14
+ end
15
+ end
16
+
17
+ def initialize(config)
18
+ @config = config
19
+ end
20
+
21
+ def load(data)
22
+ data.each_pair do |key, value|
23
+ key = key.to_s
24
+ path = Path.new(key)
25
+ case key
26
+ when 'from', 'image' then
27
+ wanted_key(path, 'from', key)
28
+ @config.from = expect_string(path, value)
29
+ when 'as_root' then
30
+ @config.as_root = expect_array(path, value)
31
+ when 'use_ssh' then
32
+ @config.use_ssh = expect_boolean(path, value)
33
+ when 'before' then
34
+ @config.before = maybe_step(path, value)
35
+ when 'forward_env' then
36
+ @config.forward_env = expect_array(path, value)
37
+ when 'export_dir' then
38
+ @config.export_dir = maybe_string(path, value)
39
+ when 'link', 'links' then
40
+ @config.links = expect_links(path, value)
41
+ when 'run', 'command' then
42
+ wanted_key(path, 'run', key)
43
+ @config.run = expect_string(path, value)
44
+ else
45
+ unknown_key(path)
46
+ end
47
+ end
48
+ @config
49
+ end
50
+
51
+ protected
52
+
53
+ def wanted_key(path, wanted_key, key)
54
+ return if key == wanted_key
55
+ @config.warnings <<
56
+ "#{path} is deprecated. use '#{wanted_key}' instead."
57
+ end
58
+
59
+ def unknown_key(path)
60
+ @config.warnings << "Unknown key #{path}"
61
+ end
62
+
63
+ def type_error(path, wanted_type, value)
64
+ @config.errors <<
65
+ "expected #{path} to be a #{wanted_type} but got a #{value.class}"
66
+ end
67
+
68
+ def expect_string(path, value)
69
+ case value
70
+ when String, Symbol, Integer
71
+ value.to_s
72
+ else
73
+ type_error(path, 'string', value)
74
+ ''
75
+ end
76
+ end
77
+
78
+ def maybe_string(path, value)
79
+ case value
80
+ when String, Symbol
81
+ value.to_s
82
+ when nil
83
+ nil
84
+ else
85
+ type_error(path, 'string or nil', value)
86
+ nil
87
+ end
88
+ end
89
+
90
+ def maybe_step(path, value)
91
+ case value
92
+ when String, Symbol, Array then
93
+ load_step(path, run: value)
94
+ when Hash then
95
+ load_step(path, value)
96
+ when nil then
97
+ nil
98
+ else
99
+ type_error(path, 'string, array, hash or nil', value)
100
+ nil
101
+ end
102
+ end
103
+
104
+ def load_step(path, data)
105
+ step = Config::Step.new
106
+ data.each_pair do |key, value|
107
+ key = key.to_s
108
+ path_ = path.append(key)
109
+ case key
110
+ when 'run' then
111
+ step.run = expect_array(path_, value)
112
+ when 'forward_env' then
113
+ step.forward_env = expect_array(path_, value)
114
+ when 'add' then
115
+ step.add = expect_array(path_, value)
116
+ else
117
+ unknown_key(path_)
118
+ end
119
+ end
120
+ step
121
+ end
122
+
123
+ def expect_links(path, value)
124
+ array = []
125
+ case value
126
+ when String, Symbol, Hash then
127
+ array << expect_link(path, value)
128
+ when Array then
129
+ value.compact.each_with_index do |value_, i|
130
+ array << expect_link(path.append(i), value_)
131
+ end
132
+ when nil then
133
+ # nothing to do
134
+ else
135
+ type_error(path, 'string, array of links, hash or nil', value)
136
+ end
137
+ array.compact
138
+ end
139
+
140
+ def expect_link(path, value)
141
+ case value
142
+ when String, Symbol then
143
+ load_link(path, name: value, image: value)
144
+ when Hash
145
+ load_link(path, value)
146
+ else
147
+ type_error(path, 'string or hash expected', value)
148
+ end
149
+ end
150
+
151
+ def load_link(path, data)
152
+ link = Config::Link.new
153
+ data.each_pair do |key, value|
154
+ key = key.to_s
155
+ path_ = path.append(key)
156
+ case key
157
+ when 'name' then
158
+ link.name = expect_string(path_, value)
159
+ when 'image', 'from' then
160
+ wanted_key(path_, 'image', key)
161
+ link.image = expect_string(path_, value)
162
+ when 'env' then
163
+ link.env = expect_env(path_, value)
164
+ when 'run' then
165
+ link.run = maybe_string(path_, value)
166
+ else
167
+ unknown_key(path_)
168
+ end
169
+ end
170
+ link.name ||= link.image
171
+ link.image ||= link.name
172
+ if link.name.nil?
173
+ type_error(
174
+ path,
175
+ 'expected hash to either declare the name or image',
176
+ data,
177
+ )
178
+ return nil
179
+ end
180
+ link
181
+ end
182
+
183
+ def expect_boolean(path, value)
184
+ case value
185
+ when true then
186
+ true
187
+ when false then
188
+ false
189
+ else
190
+ type_error(path, 'boolean', value)
191
+ false
192
+ end
193
+ end
194
+
195
+ def expect_array(path, value)
196
+ array = []
197
+ case value
198
+ when Array then
199
+ value.compact.each_with_index do |value_, i|
200
+ array << expect_string(path.append(i), value_)
201
+ end
202
+ when String, Symbol then
203
+ array << value.to_s
204
+ when nil then
205
+ # nothing to do
206
+ else
207
+ type_error(path, 'array of string, string or nil', value)
208
+ end
209
+ array.compact
210
+ end
211
+
212
+ def expect_env(path, value)
213
+ case value
214
+ when Hash then
215
+ hash = {}
216
+ value.each_pair do |key, value_|
217
+ hash[key.to_s] = expect_string(path.append(key.to_s), value_)
218
+ end
219
+ hash
220
+ else
221
+ type_error(path, 'hash', value)
222
+ {}
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
data/lib/cide/build.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'cide/build/config'
2
+ require 'cide/build/config_loader'
data/lib/cide/cli.rb ADDED
@@ -0,0 +1,195 @@
1
+ require 'cide/constants'
2
+ require 'cide/docker'
3
+ require 'cide/build'
4
+
5
+ require 'thor'
6
+
7
+ require 'json'
8
+ require 'time'
9
+
10
+ module CIDE
11
+ # Command-line option-parsing and execution for cide
12
+ class CLI < Thor
13
+ include CIDE::Docker
14
+ include Thor::Actions
15
+ add_runtime_options!
16
+
17
+ default_command 'build'
18
+
19
+ desc 'build', 'Builds an image and executes the run script'
20
+
21
+ method_option 'name',
22
+ desc: 'Name of the build',
23
+ aliases: %w(n t),
24
+ default: File.basename(Dir.pwd)
25
+
26
+ method_option 'export',
27
+ desc: 'Whenever to export artifacts',
28
+ type: :boolean,
29
+ default: nil
30
+
31
+ method_option 'export_dir',
32
+ desc: 'Change the ouput directory on the host',
33
+ aliases: %w(o host_export_dir),
34
+ default: nil
35
+
36
+ method_option 'run',
37
+ desc: 'Override the script to run',
38
+ aliases: ['r'],
39
+ default: nil
40
+
41
+ method_option 'ssh_key',
42
+ desc: 'Path to a ssh key to import into the docker image',
43
+ aliases: ['s'],
44
+ default: '~/.ssh/id_rsa'
45
+
46
+ def build
47
+ containers = []
48
+
49
+ setup_docker
50
+
51
+ ## Config ##
52
+ banner 'Config'
53
+ build = Build::Config.load_file CONFIG_FILE
54
+ exit 1 if build.nil?
55
+ export_dir = options.export_dir || File.dirname(build.export_dir)
56
+ build.run = options.run if options.run
57
+ name = CIDE::Docker.id options.name
58
+ tag = "cide/#{name}"
59
+ say_status :config, build.inspect
60
+
61
+ ## Build ##
62
+ banner 'Build'
63
+ if build.use_ssh
64
+ unless File.exist?(options.ssh_key)
65
+ fail ArgumentError, "SSH key #{options.ssh_key} not found"
66
+ end
67
+
68
+ create_tmp_file SSH_CONFIG_FILE, File.read(SSH_CONFIG_PATH)
69
+ create_tmp_file TEMP_SSH_KEY, File.read(build.ssh_key)
70
+ end
71
+ create_tmp_file DOCKERFILE, build.to_dockerfile
72
+ docker :build, '--force-rm', '--pull', '-f', DOCKERFILE, '-t', tag, '.'
73
+
74
+ ## CI ##
75
+ banner 'Run'
76
+ build.links.each do |link|
77
+ args = ['--detach']
78
+ link.env.each_pair do |key, value|
79
+ args.push('--env', [key, value].join('='))
80
+ end
81
+ args << link.image
82
+ args << link.run if link.run
83
+ link.id = docker(:run, *args, capture: true).strip
84
+ containers << link.id
85
+ end
86
+
87
+ run_options = ['--detach']
88
+
89
+ build.forward_env.each do |env|
90
+ run_options.push '--env', [env, ENV[env]].join('=')
91
+ end
92
+
93
+ build.links.each do |link|
94
+ run_options.push '--link', [link.id, link.name].join(':')
95
+ end
96
+
97
+ run_options.push tag
98
+ run_options.push build.run
99
+
100
+ id = docker(:run, *run_options, capture: true).strip
101
+ containers << id
102
+ docker(:attach, id)
103
+
104
+ ## Export ##
105
+ return unless options.export
106
+ banner 'Export'
107
+ fail 'export flag set but no export_dir given' if build.export_dir.nil?
108
+
109
+ guest_export_dir = File.expand_path(build.export_dir, CIDE_SRC_DIR)
110
+ host_export_dir = File.expand_path(export_dir, Dir.pwd)
111
+ docker :cp, [id, guest_export_dir].join(':'), host_export_dir
112
+ rescue Docker::Error => ex
113
+ exit ex.exitstatus
114
+ ensure
115
+ # Shutdown old containers
116
+ unless containers.empty?
117
+ docker :rm, '--force', *containers.reverse,
118
+ verbose: false,
119
+ capture: true
120
+ end
121
+ end
122
+
123
+ desc 'clean', 'Removes old containers'
124
+ method_option 'days',
125
+ desc: 'Number of days to keep the images',
126
+ default: 7,
127
+ type: :numeric
128
+ method_option 'count',
129
+ desc: 'Maximum number of images to keep',
130
+ default: 10,
131
+ type: :numeric
132
+ def clean
133
+ setup_docker
134
+
135
+ days_to_keep = options[:days]
136
+ max_images = options[:count]
137
+
138
+ x = docker('images', '--no-trunc', capture: true)
139
+ iter = x.lines.each
140
+ iter.next
141
+ cide_image_ids = iter
142
+ .map { |line| line.split(/\s+/) }
143
+ .select { |line| line[0] =~ %r{^cide/} || line[0] == '<none>' }
144
+ .map { |line| line[2] }
145
+
146
+ if cide_image_ids.empty?
147
+ puts 'No images found to be cleaned'
148
+ return
149
+ end
150
+
151
+ x = docker('inspect', *cide_image_ids, capture: true)
152
+ cide_images = JSON.parse(x.strip)
153
+ .each { |image| image['Created'] = Time.iso8601(image['Created']) }
154
+ .sort { |a, b| a['Created'] <=> b['Created'] }
155
+
156
+ if cide_images.size > max_images
157
+ old_cide_images = cide_images[0..-max_images]
158
+ .map { |image| image['Id'] }
159
+ else
160
+ old_times = Time.now - (days_to_keep * 24 * 60 * 60)
161
+ old_cide_images = cide_images
162
+ .select { |image| image['Created'] < old_times }
163
+ .map { |image| image['Id'] }
164
+ end
165
+
166
+ if old_cide_images.empty?
167
+ puts 'No images found to be cleaned'
168
+ return
169
+ end
170
+
171
+ docker('rmi', *old_cide_images)
172
+ end
173
+
174
+ desc 'init', "Creates a blank #{CONFIG_FILE} into the project"
175
+ def init
176
+ puts "Creating #{CONFIG_FILE} with default values"
177
+ create_file CONFIG_FILE, File.read(DEFAULT_CIDEFILE)
178
+ end
179
+
180
+ private
181
+
182
+ def create_tmp_file(destination, *args, &block)
183
+ create_file(destination, *args, &block)
184
+ at_exit do
185
+ remove_file(destination, verbose: false)
186
+ end
187
+ end
188
+
189
+ LINE_SIZE = 78.0
190
+ def banner(text)
191
+ pad = (LINE_SIZE - text.size - 4) / 2
192
+ puts '=' * pad.floor + "[ #{text} ]" + '=' * pad.ceil
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,12 @@
1
+ module CIDE
2
+ dir = File.expand_path('..', __FILE__)
3
+ DOCKERFILE = 'Dockerfile.cide'
4
+ DEFAULT_CIDEFILE = File.join(dir, 'default_cide.yml')
5
+ TEMP_SSH_KEY = 'id_rsa.tmp'
6
+ SSH_CONFIG_FILE = 'ssh_config'
7
+ SSH_CONFIG_PATH = File.join(dir, SSH_CONFIG_FILE)
8
+ CONFIG_FILE = '.cide.yml'
9
+ CIDE_DIR = '/cide'
10
+ CIDE_SRC_DIR = File.join(CIDE_DIR, 'src')
11
+ CIDE_SSH_DIR = File.join(CIDE_DIR, '.ssh')
12
+ end
@@ -0,0 +1,9 @@
1
+ ---
2
+ # See https://github.com/zimbatm/cide
3
+ from: ubuntu
4
+ as_root: []
5
+ forward_env: []
6
+ before:
7
+ use_ssh: false
8
+ export_dir:
9
+ run: script/ci
data/lib/cide/docker.rb CHANGED
@@ -17,6 +17,8 @@ module CIDE
17
17
  end
18
18
  end
19
19
 
20
+ class VersionError < StandardError; end
21
+
20
22
  def docker(*args, **opts)
21
23
  setup_docker
22
24
 
@@ -42,6 +44,13 @@ module CIDE
42
44
  .lines
43
45
  .grep(/export (\w+)=(.*)/) { ENV[$1] = $2.strip }
44
46
  end
47
+
48
+ # Check docker version
49
+ unless `docker version 2>/dev/null` =~ /Client version: ([^\s]+)/
50
+ fail VersionError, 'Unknown docker version'
51
+ end
52
+ fail VersionError, "Docker version #{$1} too old" if $1 < '1.5.0'
53
+
45
54
  true
46
55
  )
47
56
  end
@@ -26,16 +26,16 @@ RUN chown -R cide:cide <%= CIDE_DIR %>
26
26
 
27
27
  # Before
28
28
 
29
- <% if before['run'] -%>
30
- <% before['forward_env'].to_a.each do |key| -%>
29
+ <% if before -%>
30
+ <% before.forward_env.each do |key| -%>
31
31
  ENV <%= key %> <%= ENV[key] %>
32
32
  <% end %>
33
- <% before['add'].to_a.each do |file| -%>
33
+ <% before.add.each do |file| -%>
34
34
  ADD <%= file %> <%= File.expand_path(file, CIDE_SRC_DIR) %>
35
35
  <% end %>
36
36
  RUN chown -R cide:cide <%= CIDE_DIR %>
37
37
  USER cide
38
- RUN <%= before['run'] %>
38
+ RUN <%= before.run %>
39
39
  USER root
40
40
  <% end -%>
41
41
 
@@ -43,14 +43,3 @@ USER root
43
43
 
44
44
  ADD . <%= CIDE_SRC_DIR %>
45
45
  RUN chown -R cide:cide <%= CIDE_DIR %>
46
-
47
- # ENV
48
-
49
- <% forward_env.to_a.each do |key| -%>
50
- ENV <%= key %> <%= ENV[key] %>
51
- <% end %>
52
-
53
- # Test !
54
-
55
- USER cide
56
- RUN <%= run %>
File without changes
data/lib/cide.rb CHANGED
@@ -1,229 +1,7 @@
1
- require 'erb'
2
- require 'json'
3
- require 'optparse'
4
- require 'time'
5
- require 'yaml'
6
-
7
- require 'thor'
8
-
9
- require 'cide/docker'
10
-
11
1
  # CIDE is a Continuous Integration Docker Environment runner
12
2
  #
13
3
  # The juicy bits are defined in CIDE::CLI
14
4
  module CIDE
15
- DIR = File.expand_path('..', __FILE__)
16
- DOCKERFILE = 'Dockerfile'
17
- TEMP_SSH_KEY = 'id_rsa.tmp'
18
- DOCKERFILE_TEMPLATE = File.join(DIR, 'cide_template.erb')
19
- SSH_CONFIG_FILE = 'ssh_config'
20
- SSH_CONFIG_PATH = File.join(DIR, SSH_CONFIG_FILE)
21
- CONFIG_FILE = '.cide.yml'
22
-
23
- CIDE_DIR = '/cide'
24
- CIDE_SRC_DIR = File.join(CIDE_DIR, '/src')
25
- CIDE_SSH_DIR = File.join(CIDE_DIR, '/.ssh')
26
-
27
- def self.struct(opts = {}, &block)
28
- Class.new(Struct.new(*opts.keys), &block).new(*opts.values)
29
- end
30
-
31
- DefaultConfig = struct(
32
- name: nil,
33
- from: 'ubuntu',
34
- as_root: [],
35
- forward_env: [],
36
- before: {},
37
- export: false,
38
- export_dir: './artifacts',
39
- host_export_dir: nil,
40
- run: 'script/ci',
41
- use_ssh: false,
42
- ssh_key: '~/.ssh/id_rsa',
43
- ) do
44
-
45
- alias_method :image=, :from=
46
- alias_method :command=, :run=
47
-
48
- def name=(str)
49
- super CIDE::Docker.id(str)
50
- end
51
-
52
- def ssh_key_path
53
- File.expand_path(ssh_key)
54
- end
55
-
56
- def to_dockerfile
57
- ERB.new(File.read(DOCKERFILE_TEMPLATE), nil, '<>-').result(binding)
58
- end
59
-
60
- def merge!(opts = {})
61
- opts.each_pair { |k, v| public_send("#{k}=", v) }
62
- self
63
- end
64
-
65
- def merge(opts = {})
66
- dup.merge!(opts)
67
- end
68
-
69
- def to_yaml
70
- members.each_with_object({}) do |k, obj|
71
- v = self[k]
72
- obj[k.to_s] = v unless v.nil?
73
- end.to_yaml
74
- end
75
- end
76
-
77
- # Command-line option-parsing and execution for cide
78
- class CLI < Thor
79
- include CIDE::Docker
80
- include Thor::Actions
81
- add_runtime_options!
82
-
83
- default_command 'build'
84
-
85
- desc 'build', 'Builds an image and executes the run script'
86
-
87
- method_option 'name',
88
- desc: 'Name of the build',
89
- aliases: %w(n t),
90
- default: nil
91
-
92
- method_option 'host_export_dir',
93
- desc: 'Output directory on host to put build artefacts in',
94
- aliases: ['o'],
95
- default: nil
96
-
97
- method_option 'export',
98
- desc: 'Are we expecting to export artifacts',
99
- type: :boolean,
100
- default: nil
101
-
102
- method_option 'run',
103
- desc: 'The script to run',
104
- aliases: ['r'],
105
- default: nil
106
-
107
- method_option 'ssh_key',
108
- desc: 'Path to a ssh key to import into the docker image',
109
- aliases: ['s'],
110
- default: '~/.ssh/id_rsa'
111
-
112
- def build
113
- setup_docker
114
-
115
- config = DefaultConfig.merge YAML.load_file(CONFIG_FILE)
116
- options.each_pair do |k, v|
117
- config[k] = v unless v.nil?
118
- end
119
- config.name ||= File.basename(Dir.pwd)
120
- config.host_export_dir ||= config.export_dir
121
-
122
- tag = "cide/#{config.name}"
123
-
124
- if config.use_ssh
125
- unless File.exist?(config.ssh_key_path)
126
- fail MalformattedArgumentError, "SSH key #{config.ssh_key} not found"
127
- end
128
-
129
- create_tmp_file SSH_CONFIG_FILE, File.read(SSH_CONFIG_PATH)
130
- create_tmp_file TEMP_SSH_KEY, File.read(config.ssh_key_path)
131
- end
132
-
133
- say_status :config, config.to_h
134
-
135
- create_tmp_file DOCKERFILE, config.to_dockerfile
136
-
137
- docker :build, '--force-rm', '-t', tag, '.'
138
-
139
- return unless config.export
140
-
141
- unless config.export_dir
142
- fail 'Fail: export flag set but no export dir given'
143
- end
144
-
145
- id = docker(:run, '-d', tag, true, capture: true).strip
146
- begin
147
- guest_export_dir = File.expand_path(config.export_dir, CIDE_SRC_DIR)
148
-
149
- host_export_dir = File.expand_path(
150
- config.host_export_dir || config.export_dir,
151
- Dir.pwd,
152
- )
153
-
154
- docker :cp, [id, guest_export_dir].join(':'), host_export_dir
155
-
156
- ensure
157
- docker :rm, '-f', id
158
- end
159
- rescue Docker::Error => ex
160
- exit ex.exitstatus
161
- end
162
-
163
- desc 'clean', 'Removes old containers'
164
- method_option 'days',
165
- desc: 'Number of days to keep the images',
166
- default: 7,
167
- type: :numeric
168
- method_option 'count',
169
- desc: 'Maximum number of images to keep',
170
- default: 10,
171
- type: :numeric
172
- def clean
173
- setup_docker
174
-
175
- days_to_keep = options[:days]
176
- max_images = options[:count]
177
-
178
- x = docker('images', '--no-trunc', capture: true)
179
- iter = x.lines.each
180
- iter.next
181
- cide_image_ids = iter
182
- .map { |line| line.split(/\s+/) }
183
- .select { |line| line[0] =~ /^cide\// || line[0] == '<none>' }
184
- .map { |line| line[2] }
185
-
186
- if cide_image_ids.empty?
187
- puts 'No images found to be cleaned'
188
- return
189
- end
190
-
191
- x = docker('inspect', *cide_image_ids, capture: true)
192
- cide_images = JSON.parse(x.strip)
193
- .each { |image| image['Created'] = Time.iso8601(image['Created']) }
194
- .sort { |a, b| a['Created'] <=> b['Created'] }
195
-
196
- if cide_images.size > max_images
197
- old_cide_images = cide_images[0..-max_images]
198
- .map { |image| image['Id'] }
199
- else
200
- old_times = Time.now - (days_to_keep * 24 * 60 * 60)
201
- old_cide_images = cide_images
202
- .select { |image| image['Created'] < old_times }
203
- .map { |image| image['Id'] }
204
- end
205
-
206
- if old_cide_images.empty?
207
- puts 'No images found to be cleaned'
208
- return
209
- end
210
-
211
- docker('rmi', *old_cide_images)
212
- end
213
-
214
- desc 'init', "Creates a blank #{CONFIG_FILE} into the project"
215
- def init
216
- puts "Creating #{CONFIG_FILE} with default values"
217
- create_file CONFIG_FILE, DefaultConfig.to_yaml
218
- end
219
-
220
- private
221
-
222
- def create_tmp_file(destination, *args, &block)
223
- create_file(destination, *args, &block)
224
- at_exit do
225
- remove_file(destination, verbose: false)
226
- end
227
- end
228
- end
229
5
  end
6
+
7
+ require 'cide/cli'
@@ -0,0 +1,116 @@
1
+ require "cide/build"
2
+ require "stringio"
3
+ require "ostruct"
4
+ require "active_support/json"
5
+
6
+ describe "CIDE::Build::Config::Loader" do
7
+ before do
8
+ @config = CIDE::Build::Config.new
9
+ @loader = CIDE::Build::ConfigLoader.new(@config)
10
+ end
11
+
12
+ default_config = {
13
+ "from" => "ubuntu",
14
+ "as_root" => [],
15
+ "use_ssh" => false,
16
+ "before" => nil,
17
+ "forward_env" => [],
18
+ "export_dir" => nil,
19
+ "links" => [],
20
+ "run" => "script/ci",
21
+ }
22
+
23
+ it "works - empty config" do
24
+ @loader.load({})
25
+ expect(@config.as_json).to eq(default_config)
26
+ expect(@config.warnings).to eq([])
27
+ expect(@config.errors).to eq([])
28
+ end
29
+
30
+ it "works2 - full config" do
31
+ full_config = {
32
+ "from" => "god",
33
+ "as_root" => ["one", "two"],
34
+ "use_ssh" => true,
35
+ "before" => {
36
+ "add" => ["zzz", "yyy"],
37
+ "forward_env" => ["HOME"],
38
+ "run" => ["a", "b"],
39
+ },
40
+ "forward_env" => ["PWD"],
41
+ "links" => [
42
+ {"name" => "redis", "image" => "redis2:foo", "env" => {"HOME" => "/"}, "run" => "redis-server"}
43
+ ],
44
+ "export_dir" => "./artifacts",
45
+ "run" => "do/something",
46
+ }
47
+
48
+ @loader.load(full_config)
49
+
50
+ expect(@config.as_json).to eq(default_config.merge(full_config))
51
+ expect(@config.warnings).to eq([])
52
+ expect(@config.errors).to eq([])
53
+ end
54
+
55
+ it "coerces things around" do
56
+ @loader.load(
57
+ as_root: "xxxxx",
58
+ before: :zzzzz,
59
+ links: ["mysql", {image: "redis", env: {PATH: "/bin"}}, nil],
60
+ forward_env: ["HOME", nil, 555]
61
+ )
62
+
63
+ expect(@config.to_h.as_json).to eq(default_config.merge(
64
+ "as_root" => ["xxxxx"],
65
+ "before" => {
66
+ "add" => [],
67
+ "forward_env" => [],
68
+ "run" => ["zzzzz"],
69
+ },
70
+ "links" => [
71
+ {"name" => "mysql", "image" => "mysql", "run" => nil, "env" => {}},
72
+ {"name" => "redis", "image" => "redis", "run" => nil, "env" => {"PATH" => "/bin"}},
73
+ ],
74
+ "forward_env" => ["HOME", "555"],
75
+ ))
76
+ expect(@config.warnings).to eq([])
77
+ expect(@config.errors).to eq([])
78
+ end
79
+
80
+ it "notifies deprecations" do
81
+ @loader.load(
82
+ image: "foo",
83
+ command: "lol",
84
+ zzz: 4,
85
+ )
86
+ expect(@config.as_json).to eq(default_config.merge(
87
+ "from" => "foo",
88
+ "run" => "lol",
89
+ ))
90
+ expect(@config.warnings).to eq([
91
+ "image is deprecated. use 'from' instead.",
92
+ "command is deprecated. use 'run' instead.",
93
+ "Unknown key zzz",
94
+ ])
95
+ expect(@config.errors).to eq([])
96
+ end
97
+
98
+ it "reports type errors" do
99
+ @loader.load(
100
+ as_root: ["aaa", Time.now],
101
+ forward_env: {},
102
+ links: {},
103
+ )
104
+
105
+ expect(@config.to_h.as_json).to eq(default_config.merge(
106
+ "as_root" => ["aaa", ""],
107
+ ))
108
+ expect(@config.warnings).to eq([])
109
+ expect(@config.errors).to eq([
110
+ "expected as_root[1] to be a string but got a Time",
111
+ "expected forward_env to be a array of string, string or nil but got a Hash",
112
+ "expected links to be a expected hash to either declare the name or image but got a Hash",
113
+ ])
114
+ end
115
+
116
+ end
@@ -0,0 +1 @@
1
+ $:.unshift File.expand_path('../../lib', __FILE__)
metadata CHANGED
@@ -1,23 +1,51 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cide
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - zimbatm
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-27 00:00:00.000000000 Z
11
+ date: 2015-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.19.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.19.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: virtus
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
15
43
  requirement: !ruby/object:Gem::Requirement
16
44
  requirements:
17
45
  - - ">="
18
46
  - !ruby/object:Gem::Version
19
47
  version: '0'
20
- type: :runtime
48
+ type: :development
21
49
  prerelease: false
22
50
  version_requirements: !ruby/object:Gem::Requirement
23
51
  requirements:
@@ -25,7 +53,7 @@ dependencies:
25
53
  - !ruby/object:Gem::Version
26
54
  version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
- name: rake
56
+ name: rubocop
29
57
  requirement: !ruby/object:Gem::Requirement
30
58
  requirements:
31
59
  - - ">="
@@ -39,7 +67,21 @@ dependencies:
39
67
  - !ruby/object:Gem::Version
40
68
  version: '0'
41
69
  - !ruby/object:Gem::Dependency
42
- name: rubocop
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activesupport
43
85
  requirement: !ruby/object:Gem::Requirement
44
86
  requirements:
45
87
  - - ">="
@@ -63,7 +105,9 @@ extensions: []
63
105
  extra_rdoc_files: []
64
106
  files:
65
107
  - ".cide.yml"
108
+ - ".editorconfig"
66
109
  - ".gitignore"
110
+ - ".rspec"
67
111
  - ".rubocop.yml"
68
112
  - ".travis.yml"
69
113
  - CHANGELOG.md
@@ -76,9 +120,17 @@ files:
76
120
  - cide
77
121
  - cide.gemspec
78
122
  - lib/cide.rb
123
+ - lib/cide/build.rb
124
+ - lib/cide/build/config.rb
125
+ - lib/cide/build/config_loader.rb
126
+ - lib/cide/cli.rb
127
+ - lib/cide/constants.rb
128
+ - lib/cide/default_cide.yml
79
129
  - lib/cide/docker.rb
80
- - lib/cide_template.erb
81
- - lib/ssh_config
130
+ - lib/cide/dockerfile_template.erb
131
+ - lib/cide/ssh_config
132
+ - spec/build_config_loader_spec.rb
133
+ - spec/spec_helper.rb
82
134
  homepage: https://github.com/zimbatm/cide
83
135
  licenses:
84
136
  - MIT
@@ -99,8 +151,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
151
  version: '0'
100
152
  requirements: []
101
153
  rubyforge_project:
102
- rubygems_version: 2.4.2
154
+ rubygems_version: 2.4.6
103
155
  signing_key:
104
156
  specification_version: 4
105
157
  summary: CI docker runner
106
- test_files: []
158
+ test_files:
159
+ - spec/build_config_loader_spec.rb
160
+ - spec/spec_helper.rb