construi 0.30.0 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --color
2
2
  --require spec_helper
3
+ --format documentation
data/README.md CHANGED
@@ -1,5 +1,147 @@
1
1
  # Construi
2
2
 
3
- [![Gem](https://img.shields.io/gem/v/construi.svg?style=plastic)](https://rubygems.org/gems/construi) [![Build Status](http://jenkins.mylonelybear.org/buildStatus/icon?job=construi-develop)](http://jenkins.mylonelybear.org/job/construi-develop/)
4
- [![Code Climate](https://img.shields.io/codeclimate/github/lstephen/construi.svg?style=plastic)](https://codeclimate.com/github/lstephen/construi) [![Code Climate](https://img.shields.io/codeclimate/coverage/github/lstephen/construi.svg?style=plastic)](https://codeclimate.com/github/lstephen/construi) [![Coveralls](https://img.shields.io/coveralls/lstephen/construi/develop.svg?style=plastic)](https://coveralls.io/r/lstephen/construi)
3
+ [![Gem](https://img.shields.io/gem/v/construi.svg?style=plastic)](https://rubygems.org/gems/construi)
4
+ [![Build Status](http://jenkins.mylonelybear.org/buildStatus/icon?job=construi-develop)](http://jenkins.mylonelybear.org/job/construi-develop/)
5
+ [![Code Climate](https://img.shields.io/codeclimate/github/lstephen/construi.svg?style=plastic)](https://codeclimate.com/github/lstephen/construi)
6
+ [![Coveralls](https://img.shields.io/coveralls/lstephen/construi/origin/develop.svg?style=plastic)](https://coveralls.io/r/lstephen/construi)
7
+
8
+ Construi allows you to use [Docker](http://www.docker.com) containers as your build environment.
9
+ This allows a consistent and recreatable build environment on any machine running Construi
10
+ and Docker.
11
+
12
+ This is useful for ensuring consistency between development machines.
13
+ It is also useful in a CI environment where you may need to build projects from many different
14
+ languages and/or versions (e.g., Java 6, Java 8, Ruby).
15
+
16
+ ## Installation
17
+
18
+ Construi requires [Ruby](http://www.ruby-lang.org) version 1.9 or higher.
19
+ It can be installed as a Gem.
20
+
21
+ ```
22
+ > gem install construi
23
+ ```
24
+
25
+ ## Running
26
+
27
+ Construi requires that a `construi.yml` file be present in the root directory of your project.
28
+ Targets can then be run by specifying them on the command line. For example:
29
+
30
+ ```
31
+ > construi build
32
+ > construi build install
33
+ ```
34
+
35
+ Construi will create a Docker container with the project directory as a volume.
36
+ It will then run the commands configured for the given targets.
37
+
38
+ ## The Construi File
39
+
40
+ As a minimal `construi.yml` requires an image and a target to be configured.
41
+ For example a simple configuration for a Java 8 project built with Maven could be:
42
+
43
+ ```
44
+ image: maven:3-jdk-8
45
+
46
+ targets:
47
+ install: mvn install
48
+ ```
49
+
50
+ Construi is built using itself, so it's
51
+ [`construi.yml`](https://github.com/lstephen/construi/blob/develop/construi.yml)
52
+ can be used as an example also.
53
+
54
+ ### Image
55
+
56
+ Specifies an image to be pulled that will be used as the build environment.
57
+ It can also be given on a per target basis.
58
+
59
+ ```
60
+ image: maven:3-jdk-7
61
+
62
+ targets:
63
+ install: mvn install
64
+
65
+ test-java-8:
66
+ image: maven:3-jdk-8
67
+ run: mvn verify
68
+ ```
69
+
70
+ ### Build
71
+
72
+ Specifies a directory containing a `Dockerfile`.
73
+ Construi will build a Docker container based on that `Dockerfile` and use it as the build
74
+ environment.
75
+ Can be used as an alternative to providing an image.
76
+ Can also be given on a per target basis.
77
+
78
+ ```
79
+ build: etc/build_environment
80
+
81
+ targets:
82
+ build:
83
+ - mvn install
84
+ - /usr/local/bin/custom_installed_command.sh
85
+ ```
86
+
87
+ ### Environment
88
+
89
+ Declares environment variables that will be passed through or set in the build environment.
90
+ If no value is provided then the value from the host environment will be used.
91
+ In this example `NEXUS_SERVER_URL` will be set as provided, while `NEXUS_USERNAME` and
92
+ `NEXUS_PASSWORD` will be retrieved from the host.
93
+
94
+ ```
95
+ image: maven:3-jdk-7
96
+
97
+ environment:
98
+ - NEXUS_SERVER_URL=http://nexus.example.com
99
+ - NEXUS_USERNAME
100
+ - NEXUS_PASSWORD
101
+ targets:
102
+ build: mvn install
103
+ ```
104
+
105
+ ### Files
106
+
107
+ Declares files to be copied into the build environment before the build is run.
108
+ Also allows setting of permissions.
109
+ Can be used on a per target basis.
110
+
111
+ ```
112
+ image: maven:3-jdk-7
113
+
114
+ files:
115
+ - etc/maven-settings.xml:/home/root/.m2/settings.xml
116
+
117
+ targets:
118
+ deploy:
119
+ files:
120
+ - $GIT_SSH_KEY:/home/root/.ssh/id_rsa:0600
121
+ run: scripts/construi/deploy.sh
122
+ ```
123
+
124
+ ### Targets
125
+
126
+ Any number of targets can be specified.
127
+ Each must specify at least one command.
128
+ If additional configuration is required for a target then the commands should be provided
129
+ under the `run` key.
130
+ If more than one command is required then a YAML list should be used.
131
+
132
+ ```
133
+ image: maven:3-jdk-7
134
+
135
+ targets:
136
+ build: mvn install
137
+
138
+ test-java-8:
139
+ image: maven:3-jdk-8
140
+ run: mvn verify
141
+
142
+ deploy:
143
+ - mvn deploy
144
+ - curl http://ci.example.com/deploy/trigger
145
+ ```
146
+
5
147
 
data/construi.gemspec CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency 'docker-api', '~> 1.20'
22
+ spec.add_dependency 'colorize', '~> 0.7.5'
22
23
 
23
24
  spec.add_development_dependency 'bundler', '~> 1.7'
24
25
  spec.add_development_dependency 'codeclimate-test-reporter'
data/construi.yml CHANGED
@@ -6,6 +6,7 @@ environment:
6
6
  - CODECLIMATE_REPO_TOKEN
7
7
  - COVERALLS_REPO_TOKEN
8
8
  - GIT_COMMIT
9
+ - GIT_BRANCH
9
10
  - GIT_SSH_KEY
10
11
  - RUBYGEMS_API_KEY
11
12
 
data/lib/construi.rb CHANGED
@@ -1,46 +1,13 @@
1
1
  require 'construi/config'
2
- require 'construi/container'
3
- require 'construi/image'
4
- require 'construi/version'
2
+ require 'construi/runner'
5
3
 
6
- require 'docker'
7
4
  require 'yaml'
8
5
 
9
6
  module Construi
10
-
11
- class Runner
12
- def initialize(config)
13
- @config = config
14
- end
15
-
16
- def run(targets)
17
- puts "Construi version: #{Construi::VERSION}"
18
-
19
- docker_host = ENV['DOCKER_HOST']
20
- Docker.url = docker_host unless docker_host.nil?
21
-
22
- puts "Docker url: #{Docker.url}"
23
- puts "Current directory: #{Dir.pwd}"
24
-
25
- Docker.validate_version!
26
- Docker.options[:read_timeout] = 60
27
- Docker.options[:chunk_size] = 8
28
-
29
- initial_image = Image.create(@config.image) { |s| puts s }
30
-
31
- commands = targets.map { |t| @config.target(t).commands }.flatten
32
-
33
- final_image = commands.reduce(IntermediateImage.seed(initial_image)) do |image, command|
34
- puts " > #{command}"
35
- image.run(command, @config.env)
36
- end
37
-
38
- final_image.delete
39
- end
40
- end
7
+ String.disable_colorization = false
41
8
 
42
9
  def self.run(targets)
43
10
  Runner.new(Config.load_file('construi.yml')).run(targets)
44
11
  end
45
-
46
12
  end
13
+
@@ -1,31 +1,82 @@
1
1
 
2
- module Construi
2
+ module Construi::Config
3
3
 
4
- class Config
5
- private_class_method :new
4
+ module Image
5
+ def image
6
+ image_configured :image
7
+ end
6
8
 
7
- attr_reader :yaml
9
+ def build
10
+ image_configured :build
11
+ end
8
12
 
9
- def initialize(yaml)
10
- @yaml = yaml
13
+ def image_configured?
14
+ yaml.is_a?(Hash) && (yaml.has_key?('build') || yaml.has_key?('image'))
15
+ end
16
+
17
+ def image_configured(what)
18
+ image_configured? ? yaml[what.to_s] : with_parent(&what)
19
+ end
20
+ end
21
+
22
+ module Files
23
+
24
+ class File
25
+ attr_reader :container, :permissions
26
+
27
+ def initialize(host, container, permissions)
28
+ @host = host
29
+ @container = container
30
+ @permissions = permissions
31
+ end
32
+
33
+ def host
34
+ @host.gsub(/\$(\w+)/) { ENV[$1] }
35
+ end
36
+
37
+ def self.parse(str)
38
+ split = str.split(':')
39
+ File.new split[0], split[1], split[2]
40
+ end
11
41
  end
12
42
 
13
- def self.load(content)
14
- new YAML.load(content)
43
+ def files_configured?
44
+ yaml.is_a? Hash and yaml.has_key? 'files'
15
45
  end
16
46
 
17
- def self.load_file(path)
18
- new YAML.load_file(path)
47
+ def files
48
+ fs = files_configured? ? yaml['files'].map { |str| File.parse(str) } : []
49
+
50
+ with_parent([], &:files).concat fs
19
51
  end
52
+ end
20
53
 
21
- def image
22
- @yaml['image']
54
+ module Environment
55
+ include Image
56
+ include Files
57
+
58
+ def parent
59
+ nil
60
+ end
61
+
62
+ def with_parent(or_else = nil)
63
+ parent ? yield(parent) : or_else
64
+ end
65
+ end
66
+
67
+ class Global
68
+ include Environment
69
+
70
+ attr_reader :yaml
71
+
72
+ def initialize(yaml)
73
+ @yaml = yaml
23
74
  end
24
75
 
25
76
  def env
26
- return [] if @yaml['environment'].nil?
77
+ return [] if yaml['environment'].nil?
27
78
 
28
- @yaml['environment'].reduce([]) do |acc, e|
79
+ yaml['environment'].reduce([]) do |acc, e|
29
80
  key = e.partition('=').first
30
81
  value = e.partition('=').last
31
82
 
@@ -37,18 +88,39 @@ module Construi
37
88
  end
38
89
 
39
90
  def target(target)
40
- Target.new(@yaml['targets'][target])
91
+ targets = yaml['targets']
92
+
93
+ return nil if targets.nil?
94
+
95
+ return Target.new yaml['targets'][target], self
41
96
  end
42
97
  end
43
98
 
44
99
  class Target
45
- def initialize(yaml)
100
+ include Environment
101
+
102
+ attr_reader :yaml, :parent
103
+
104
+ def initialize(yaml, parent)
46
105
  @yaml = yaml
106
+ @parent = parent
47
107
  end
48
108
 
49
109
  def commands
50
- @yaml
110
+ Array(@yaml.is_a?(Hash) ? @yaml['run'] : @yaml)
51
111
  end
112
+
113
+ def env
114
+ parent.env
115
+ end
116
+ end
117
+
118
+ def self.load(content)
119
+ Global.new YAML.load(content)
52
120
  end
53
121
 
122
+ def self.load_file(path)
123
+ Global.new YAML.load_file(path)
124
+ end
54
125
  end
126
+
@@ -9,14 +9,20 @@ module Construi
9
9
  @container = container
10
10
  end
11
11
 
12
+ def id
13
+ @container.id
14
+ end
15
+
12
16
  def delete
13
17
  @container.delete
14
18
  end
15
19
 
16
20
  def attach_stdout
17
21
  @container.attach(:stream => true, :logs => true) { |s, c| puts c; $stdout.flush }
22
+ true
18
23
  rescue Docker::Error::TimeoutError
19
- puts 'Failed to attached to stdout'
24
+ puts 'Failed to attach to stdout'.yellow
25
+ false
20
26
  end
21
27
 
22
28
  def commit
@@ -25,14 +31,20 @@ module Construi
25
31
 
26
32
  def run
27
33
  @container.start
28
- attach_stdout
34
+ attached = attach_stdout
29
35
  status_code = @container.wait['StatusCode']
30
36
 
31
- raise RunError.new 'Cmd returned status code: #{status_code}' unless status_code == 0
37
+ puts @container.logs(:stdout => true) unless attached
38
+
39
+ raise Error, "Cmd returned status code: #{status_code}" unless status_code == 0
32
40
 
33
41
  commit
34
42
  end
35
43
 
44
+ def ==(other)
45
+ other.is_a? Container and id == other.id
46
+ end
47
+
36
48
  def self.create(image, cmd, env)
37
49
  wrap Docker::Container.create(
38
50
  'Cmd' => cmd.split,
@@ -58,12 +70,9 @@ module Construi
58
70
  use(image, cmd, env, &:run)
59
71
  end
60
72
 
61
- end
62
-
63
- class ContainerError < StandardError
64
- end
73
+ class Error < StandardError
74
+ end
65
75
 
66
- class RunError < ContainerError
67
76
  end
68
77
 
69
78
  end
@@ -1,5 +1,8 @@
1
1
  require 'construi/container'
2
2
 
3
+ require 'colorize'
4
+ require 'docker'
5
+
3
6
  module Construi
4
7
 
5
8
  class Image
@@ -17,40 +20,102 @@ module Construi
17
20
  @image.delete
18
21
  end
19
22
 
23
+ def docker_image
24
+ @image
25
+ end
26
+
20
27
  def tagged?
21
28
  @image.info['RepoTags'] != '<none>:<none>'
22
29
  end
23
30
 
24
- def run(cmd, env)
31
+ def insert_local(file)
32
+ puts "\nCopying #{file.host} to #{file.container}...".green
33
+
34
+ img = IntermediateImage.seed(self)
35
+
36
+ img.map do |i|
37
+ Image.wrap i.docker_image
38
+ .insert_local 'localPath' => file.host, 'outputPath' => file.container
39
+ end
40
+
41
+ img.map { |i| i.chmod file.container, file.permissions } if file.permissions
42
+
43
+ img.run "ls -l #{file.container}"
44
+
45
+ img.image
46
+ end
47
+
48
+ def insert_locals(files)
49
+ IntermediateImage.seed(self).reduce(files) { |i, f| i.insert_local f }.image
50
+ end
51
+
52
+ def chmod(file, permissions)
53
+ chmod = "chmod -R #{permissions} #{file}"
54
+
55
+ puts " > #{chmod}"
56
+ run chmod
57
+ end
58
+
59
+ def run(cmd, env = [])
25
60
  Container.run(self, cmd, env)
26
61
  end
27
62
 
63
+ def ==(other)
64
+ other.is_a? Image and id == other.id
65
+ end
66
+
67
+ def self.from(config)
68
+ image = create(config.image) unless config.image.nil?
69
+ image = build(config.build) unless config.build.nil?
70
+
71
+ raise Error, "Invalid image configuration: #{config}" unless image
72
+
73
+ image.insert_locals config.files
74
+ end
75
+
28
76
  def self.create(image)
29
- wrap Docker::Image.create('fromImage' => image)
77
+ puts
78
+ puts "Creating image: '#{image}'...".green
79
+ wrap Docker::Image.create('fromImage' => image) { |s|
80
+ status = JSON.parse(s)
81
+
82
+ id = status['id']
83
+ progress = status['progressDetail']
84
+
85
+ if progress.nil? or progress.empty?
86
+ print "#{id}: " unless id.nil?
87
+ puts "#{status['status']}"
88
+ end
89
+ }
90
+ end
91
+
92
+ def self.build(build)
93
+ puts
94
+ puts "Building image: '#{build}'...".green
95
+ wrap Docker::Image.build_from_dir(build, :rm => 0) { |s|
96
+ puts JSON.parse(s)['stream']
97
+ }
30
98
  end
31
99
 
32
100
  def self.wrap(image)
33
101
  new image
34
102
  end
35
103
 
36
- def self.use(image)
37
- begin
38
- i = create(image)
39
- yield i
40
- ensure
41
- i.delete unless i.tagged?
42
- end
104
+ class Error < StandardError
43
105
  end
44
106
  end
45
107
 
46
108
  class IntermediateImage
47
109
  private_class_method :new
48
110
 
111
+ attr_reader :image
112
+
49
113
  def initialize(image)
50
114
  @image = image
115
+ @first = true
51
116
  end
52
117
 
53
- def run(cmd, env)
118
+ def run(cmd, env = [])
54
119
  map { |i| i.run(cmd, env) }
55
120
  end
56
121
 
@@ -58,9 +123,17 @@ module Construi
58
123
  update(yield @image)
59
124
  end
60
125
 
126
+ def reduce(iter)
127
+ iter.reduce(self) do |intermediate_image, item|
128
+ intermediate_image.map { |i| yield i, item }
129
+ end
130
+ end
131
+
61
132
  def update(image)
62
- @image.delete unless @image.tagged?
133
+ delete unless @first
134
+ @first = false
63
135
  @image = image
136
+ self
64
137
  end
65
138
 
66
139
  def delete