cide 0.1.1 → 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.
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