construi 0.30.0 → 0.31.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.
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