blubber 0.2.0 → 0.3.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
  SHA256:
3
- metadata.gz: 2fc43a8d69b7d4d93a797191b720892f7b8ad4a853f450d6177cb32064a03ebc
4
- data.tar.gz: b56d0301887bc29ab7dbf07328db241f7230afc7ee101cf475350250187c1850
3
+ metadata.gz: 4f4ffd9a2666bed028135a571e27d7993fd1646656d8a18682a632ea6fcc52cc
4
+ data.tar.gz: 3e987000d6a8344b3893e5cdf0f0d9120d9f134400e1b8f99155cbf432f92916
5
5
  SHA512:
6
- metadata.gz: cb8a7675b4959203c9675d3dd1e35cc645924f8e66638e5c94d5b4a9f1d19eea96aba8c662a32c8e51e0c1b5b9d6adae157c5b471cfc274849644d2602cec767
7
- data.tar.gz: b6696c0121a6cc8fef1e541c1e4b0216171e7cfa476370b06a9fee5e74400d4fa2eab841aa129e03230413b43e5253d1fe004db7eed36eb85510bc414dd8b4fe
6
+ metadata.gz: 59125f6668acfb8cab8e06c9854f77f229c863f436566bbdf6d6578e0ea604f153b9f6f6bae56066e25789f9c98c6f257b6eb7e72c287918b4c38066ac11b2f5
7
+ data.tar.gz: bd1e86b80d26b8ac5b598e6321a46cee7d6dbedbf3948f71f6370e626c0d8ffd74208a68093853421c47296671760a8f657383652a6834e01583419a01b62e0a
@@ -0,0 +1,46 @@
1
+ require 'singleton'
2
+ require 'forwardable'
3
+
4
+ module Blubber
5
+ class BuildInfo
6
+ extend Forwardable
7
+ def_delegators :shared, :branch_name, :dirty?, :commit, :last_successful_commit
8
+
9
+ def initialize(layer:)
10
+ @layer = layer
11
+ end
12
+
13
+ def dirty?
14
+ @dirty ||= system("git status --porcelain 2>&1 | grep -q '#{layer.directory}'")
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :layer
20
+
21
+ def shared
22
+ @shared ||= Class.new do
23
+ include Singleton
24
+
25
+ def branch_name
26
+ @branch_name ||= ENV.fetch('BRANCH_NAME') do
27
+ `
28
+ git rev-parse HEAD |
29
+ git branch -a --contains |
30
+ sed -n 2p |
31
+ cut -d'/' -f 3-
32
+ `.strip
33
+ end
34
+ end
35
+
36
+ def commit
37
+ @commit ||= ENV.fetch('GIT_COMMIT') { `git rev-parse HEAD`.strip }
38
+ end
39
+
40
+ def last_successful_commit
41
+ @last_successful_commit ||= ENV['GIT_PREVIOUS_SUCCESSFUL_COMMIT']
42
+ end
43
+ end.instance
44
+ end
45
+ end
46
+ end
@@ -1,25 +1,59 @@
1
1
  require 'highline'
2
2
  require 'logger'
3
- require 'open3'
4
3
 
5
4
  require 'blubber/runner'
6
- require 'blubber/tagger'
5
+ require 'blubber/logger'
7
6
 
8
7
  module Blubber
9
8
  class Builder
10
9
  def initialize(layer:, logger: nil)
11
10
  @layer = layer
12
- @logger = logger
11
+ @logger = logger || Logger.for(name: layer.name)
13
12
  end
14
13
 
15
- def run
16
- logger.info ui.color("BUILDING", :yellow)
17
- retval = build(layer)
18
- level, color = retval.zero? ? [:info, :green] : [:error, :red]
14
+ def build
15
+ logger.info ui.color('BUILDING', :yellow)
16
+ retval = do_build
17
+ level, color = retval.zero? ? %i[info green] : %i[error red]
19
18
 
20
- logger.public_send(level, ui.color("#{layer}: #{retval.zero? ? 'SUCCESS' : 'ERROR'}", color))
19
+ logger.public_send(level,
20
+ ui.color((retval.zero? ? 'SUCCESS' : 'ERROR'), color))
21
21
 
22
- { success: retval.zero?, id: build_ids[layer] }
22
+ retval.zero?
23
+ end
24
+
25
+ def tag
26
+ logger.info ui.color('TAGGING', :yellow)
27
+ status = {}
28
+ layer.tags.each do |tag|
29
+ status[tag] = runner.run("docker tag #{layer.build_id} #{layer.project_tag(tag)}")
30
+ end
31
+
32
+ retval = status.values.reduce(:+)
33
+
34
+ level, color = retval.zero? ? %i[info green] : %i[error red]
35
+
36
+ logger.public_send(level,
37
+ ui.color((retval.zero? ? 'SUCCESS' : 'ERROR'), color))
38
+
39
+ retval.zero?
40
+ end
41
+
42
+ def push
43
+ logger.info ui.color('PUSHING', :yellow)
44
+ status = {}
45
+ layer.tags.each do |tag|
46
+ status[tag] = runner.run("docker push #{layer.project_tag(tag)}")
47
+ end
48
+
49
+ retval = status.values.reduce(:+)
50
+
51
+ level, color = retval.zero? ? %i[info green] : %i[error red]
52
+
53
+ logger.public_send(level,
54
+ ui.color((retval.zero? ? 'SUCCESS' : 'ERROR'), color))
55
+
56
+ retval.zero?
23
57
  end
24
58
 
25
59
  private
@@ -34,23 +68,22 @@ module Blubber
34
68
  @runner ||= Runner.new(logger: logger)
35
69
  end
36
70
 
37
- def tagger
38
- @tagger ||= Tagger.new(layer: layer, image_id: nil)
39
- end
40
-
41
- def build_ids
42
- @build_ids ||= {}
43
- end
44
-
45
- def build(layer)
71
+ def do_build
46
72
  status = nil
47
- Dir.chdir(layer) do
48
- status = runner.run("docker build --build-arg BRANCH_NAME=#{tagger.branch_name} .") do |stdout, _, _|
73
+ Dir.chdir(layer.directory) do
74
+ cmd = []
75
+ cmd += %w[docker build]
76
+ cmd += %W[--build-arg VERSION=#{layer.build_tag}]
77
+ cmd += %W[--cache-from #{layer.cache}] if layer.cache
78
+ cmd << '.'
79
+
80
+ status = runner.run(cmd) do |stdout, _, _|
49
81
  if stdout && (m = stdout.match(/Successfully built ([a-z0-9]{12})/))
50
- build_ids[layer] = m[1]
82
+ layer.build_id = m[1]
51
83
  end
52
84
  end
53
85
  end
86
+
54
87
  status
55
88
  end
56
89
  end
@@ -1,12 +1,24 @@
1
1
  require 'thor'
2
2
 
3
3
  require 'blubber/flow'
4
+ require 'blubber/context'
4
5
 
5
6
  module Blubber
6
7
  class Cli < Thor
7
- desc 'build', 'Builds all found Docker images'
8
- def build
9
- exit(Flow.build) # Fails to build if any layer fails
8
+ class_option :registry, aliases: %w(-r), desc: 'Docker Registry', default: ENV['DOCKER_REGISTRY']
9
+
10
+ desc 'build LAYER [LAYER...]', 'Builds all found Docker images'
11
+ method_option :tag, aliases: %w(-t), desc: 'Tag all built images', default: true
12
+ method_option :push, aliases: %w(-p), desc: 'Push images as they are built', default: true
13
+ def build(layers = nil)
14
+ flow = Flow.new(
15
+ layers: layers,
16
+ build: true,
17
+ tag: options.tag,
18
+ push: options.push,
19
+ context: Context.new(docker_registry: options.registry)
20
+ )
21
+ exit flow.run
10
22
  end
11
23
  end
12
24
  end
@@ -0,0 +1,12 @@
1
+ require 'singleton'
2
+ require 'forwardable'
3
+
4
+ module Blubber
5
+ class Context
6
+ attr_reader :docker_registry
7
+
8
+ def initialize(docker_registry: nil)
9
+ @docker_registry = docker_registry || ENV.fetch('DOCKER_REGISTRY')
10
+ end
11
+ end
12
+ end
@@ -1,83 +1,48 @@
1
1
  require 'highline'
2
- require 'open3'
3
2
 
4
- require 'blubber/builder'
5
- require 'blubber/tagger'
3
+ require 'blubber/layer'
6
4
 
7
5
  module Blubber
8
6
  class Flow
9
- def self.build(layers = nil)
10
- layers ||= changed_layers
7
+ def initialize(layers:, context:, build: true, tag: true, push: true)
8
+ @context = context
9
+ @layers = detect_layers(layers: layers)
10
+ @flow = { build: build, tag: tag, push: push }
11
+ end
11
12
 
12
- puts "Building layers: #{layers.join(', ')}"
13
+ def run
14
+ results = layers.map do |layer|
15
+ status = true
16
+ status &= layer.build if flow[:build]
17
+ status &= layer.tag if flow[:tag] && layer.build_id
18
+ status &= layer.push if flow[:push] && layer.build_id
13
19
 
14
- images = layers.map { |layer| Flow.new(layer: layer).run }
20
+ [layer, status]
21
+ end.to_h
15
22
 
16
23
  table = [HighLine.color('Layer', :bold), HighLine.color('Tag', :bold)]
17
- table += images.map do |image|
18
- if image[:success]
19
- image[:tags].map { |tag| [image[:project], tag] }
24
+ table += results.map do |layer, result|
25
+ if result
26
+ layer.tags.map { |tag| [layer.project, tag] }
20
27
  else
21
- [image[:project], HighLine.color('FAILED', :red)]
28
+ [layer.project, HighLine.color('FAILED', :red)]
22
29
  end
23
30
  end
24
-
25
31
  puts HighLine.new.list(table.flatten, :columns_across, 2)
26
32
 
27
- images.all? { |image| image[:success] }
28
- end
29
-
30
- def self.changed_layers
31
- @changed_layers ||= begin
32
- if ENV.fetch('GIT_PREVIOUS_SUCCESSFUL_COMMIT', '').empty? || ENV['BUILD_ALL'] == 'true'
33
- Dir['**/*/Dockerfile'].map { |d| File.dirname(d) }.sort
34
- else
35
- commit = ENV['GIT_COMMIT'] || `git rev-parse HEAD`.strip
36
-
37
- puts "Detecting changed layers between #{ENV['GIT_PREVIOUS_SUCCESSFUL_COMMIT']}..#{commit}"
38
-
39
- changes = `git diff --name-only #{ENV['GIT_PREVIOUS_SUCCESSFUL_COMMIT']}..#{commit}`.split("\n")
40
- paths = []
41
- changes.each do |path|
42
- dirs = File.dirname(path).split(File::SEPARATOR)
43
- dirs.map.with_index { |_, i| dirs[0..i].join(File::SEPARATOR) }.reverse.each do |dir|
44
- paths << dir if File.exist?(File.join(dir, 'Dockerfile'))
45
- end
46
- end
47
- paths
48
- end
49
- end
50
- end
51
-
52
- def initialize(layer:)
53
- @layer = layer
54
- end
55
-
56
- def run
57
- image = Builder.new(layer: layer, logger: logger).run
58
-
59
- tagger = Tagger.new(layer: layer, image_id: image[:id], logger: logger)
60
- tagger.run if image[:success]
61
-
62
- image.merge(project: tagger.project, tags: tagger.tags)
33
+ results.values.reduce(:&)
63
34
  end
64
35
 
65
36
  private
66
37
 
67
- attr_reader :layer
38
+ attr_reader :layers, :context, :flow
68
39
 
69
- def logger
70
- STDOUT.sync = true
71
- @logger ||= Logger.new(STDOUT).tap do |logger|
72
- logger.progname = layer
73
- logger.formatter = proc do |severity, datetime, progname, msg|
74
- format("%<severity>s, [%<datetime>s] -- %<progname>s: %<msg>s\n",
75
- severity: severity[0],
76
- datetime: datetime.strftime('%Y-%m-%d %H:%M:%S'),
77
- progname: progname,
78
- msg: msg)
79
- end
80
- end
40
+ def detect_layers(layers: nil)
41
+ Dir['**/*/Dockerfile']
42
+ .map { |d| File.dirname(d) }
43
+ .sort
44
+ .select { |layer| layers.nil? || layers.include?(layer) }
45
+ .map { |layer| Layer.new(name: layer, directory: File.expand_path('.', layer), context: context) }
81
46
  end
82
47
  end
83
48
  end
@@ -0,0 +1,107 @@
1
+ require 'blubber/build_info'
2
+ require 'blubber/builder'
3
+ require 'blubber/runner'
4
+
5
+ module Blubber
6
+ class Layer
7
+ attr_reader :directory, :name
8
+ attr_accessor :build_id
9
+
10
+ def initialize(context:, directory:, name:)
11
+ @context = context
12
+ @directory = Pathname.new(directory)
13
+ @name = name
14
+ end
15
+
16
+ # Actions
17
+ def build
18
+ builder.build
19
+ end
20
+
21
+ def tag
22
+ builder.tag
23
+ end
24
+
25
+ def push
26
+ builder.push
27
+ end
28
+
29
+ # Accessors
30
+ def repo
31
+ @repo ||= name.split('/').select { |p| p[/[a-z0-9]+/] }.join('/')
32
+ end
33
+
34
+ def project
35
+ "#{context.docker_registry}/#{repo}"
36
+ end
37
+
38
+ def project_tag(tag)
39
+ "#{project}:#{tag}"
40
+ end
41
+
42
+ def cache
43
+ @cache ||= begin
44
+ cache_tags = []
45
+
46
+ if build_info.last_successful_commit
47
+ cache_tags << build_info.last_successful_commit.to_s
48
+ end
49
+ cache_tags << build_tag.to_s if build_tag
50
+ cache_tags << branch_tag.to_s if branch_tag
51
+ cache_tags << 'latest'
52
+
53
+ cache_tags
54
+ .map { |tag| "#{project}:#{tag}" }
55
+ .find { |img| runner.run("docker pull #{img}").zero? }
56
+ end
57
+ end
58
+
59
+ def tags
60
+ @tags ||= begin
61
+ tags = []
62
+ tags << build_info.commit
63
+
64
+ unless build_info.dirty?
65
+ tags << branch_tag if branch_tag
66
+ tags << 'latest' if branch_tag == 'master'
67
+ end
68
+
69
+ tags << File.read(directory.join('Dockerfile')).scan(/LABEL version=([\w][\w.-]*)/)
70
+
71
+ tags.flatten.map { |t| "#{t}#{build_info.dirty? ? '-dirty' : ''}" }
72
+ end
73
+ end
74
+
75
+ def branch_tag
76
+ filter(build_info.branch_name) unless build_info.branch_name.empty?
77
+ end
78
+
79
+ def build_tag
80
+ branch_tag || build_info.commit
81
+ end
82
+
83
+ private
84
+
85
+ attr_reader :context
86
+
87
+ def builder
88
+ @builder ||= Builder.new(layer: self, logger: logger)
89
+ end
90
+
91
+ def filter(thing)
92
+ thing.gsub(/[^\w.-]/, '_')
93
+ end
94
+
95
+ def build_info
96
+ @build_info ||= BuildInfo.new(layer: self)
97
+ end
98
+
99
+ def runner
100
+ @runner ||= Runner.new(logger: logger)
101
+ end
102
+
103
+ def logger
104
+ Logger.for(name: name)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,35 @@
1
+ require 'singleton'
2
+ require 'forwardable'
3
+ require 'logger'
4
+
5
+ module Blubber
6
+ class Logger
7
+ include Singleton
8
+
9
+ class << self
10
+ extend Forwardable
11
+ def_delegators :instance, :for
12
+ end
13
+
14
+ def for(name:)
15
+ loggers.fetch(name) do
16
+ ::Logger.new(STDOUT).tap do |logger|
17
+ logger.progname = name
18
+ logger.formatter = proc do |severity, datetime, progname, msg|
19
+ format("%<severity>s, [%<datetime>s] -- %<progname>s: %<msg>s\n",
20
+ severity: severity[0],
21
+ datetime: datetime.strftime('%Y-%m-%d %H:%M:%S'),
22
+ progname: progname,
23
+ msg: msg)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def loggers
32
+ @loggers ||= {}
33
+ end
34
+ end
35
+ end
@@ -6,9 +6,10 @@ module Blubber
6
6
  @logger = logger
7
7
  end
8
8
 
9
- def run(cmd)
9
+ def run(*cmd)
10
+ logger.info "Running #{cmd}"
10
11
  # see: http://stackoverflow.com/a/1162850/83386
11
- Open3.popen3(cmd) do |_stdin, stdout, stderr, thread|
12
+ Open3.popen3(cmd.join(' ')) do |_stdin, stdout, stderr, thread|
12
13
  # read each stream from a new thread
13
14
  { out: stdout, err: stderr }.each do |key, stream|
14
15
  Thread.new do
@@ -1,3 +1,3 @@
1
1
  module Blubber
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blubber
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikko Kokkonen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-05-21 00:00:00.000000000 Z
11
+ date: 2018-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -99,11 +99,14 @@ files:
99
99
  - blubber.gemspec
100
100
  - exe/blubber
101
101
  - lib/blubber.rb
102
+ - lib/blubber/build_info.rb
102
103
  - lib/blubber/builder.rb
103
104
  - lib/blubber/cli.rb
105
+ - lib/blubber/context.rb
104
106
  - lib/blubber/flow.rb
107
+ - lib/blubber/layer.rb
108
+ - lib/blubber/logger.rb
105
109
  - lib/blubber/runner.rb
106
- - lib/blubber/tagger.rb
107
110
  - lib/blubber/version.rb
108
111
  homepage: https://github.com/mikian/blubber
109
112
  licenses: []
@@ -1,86 +0,0 @@
1
- require 'highline'
2
- require 'logger'
3
- require 'open3'
4
-
5
- require 'blubber/runner'
6
-
7
- module Blubber
8
- class Tagger
9
- def self.docker_registry
10
- ENV.fetch('DOCKER_REGISTRY')
11
- end
12
-
13
- def initialize(layer:, image_id:, logger: nil)
14
- @layer = layer
15
- @image_id = image_id
16
- @logger = logger
17
- end
18
-
19
- def run
20
- logger.info ui.color("#{layer}: PUSHING", :yellow)
21
-
22
- push
23
- end
24
-
25
- def tags
26
- @tags ||= begin
27
- tags = []
28
- tags << commit
29
-
30
- unless dirty?
31
- tags << branch_name.gsub(/[^\w.-]/, '_') unless branch_name.empty?
32
- tags << 'latest' if branch_name == 'master'
33
- end
34
-
35
- tags << File.read("#{layer}/Dockerfile").scan(/LABEL version=([\w][\w.-]*)/)
36
-
37
- tags.flatten.map { |t| "#{t}#{dirty? ? '-dirty' : ''}" }
38
- end
39
- end
40
-
41
- def project
42
- [
43
- Tagger.docker_registry,
44
- *layer.split('/').select { |p| p[/[a-z0-9]+/] }
45
- ].join('/')
46
- end
47
-
48
- def branch_name
49
- @branch_name ||= ENV['BRANCH_NAME'] || `git rev-parse HEAD | git branch -a --contains | sed -n 2p | cut -d'/' -f 3-`.strip
50
- end
51
-
52
- private
53
-
54
- attr_reader :layer, :image_id, :logger
55
-
56
- def runner
57
- @runner ||= Runner.new(logger: logger)
58
- end
59
-
60
- def ui
61
- @ui ||= HighLine.new
62
- end
63
-
64
- def dirty?
65
- @dirty ||= system("git status --porcelain 2>&1 | grep -q '#{layer}'")
66
- end
67
-
68
- def commit
69
- @commit ||= ENV['GIT_COMMIT'] || `git rev-parse HEAD`.strip
70
- end
71
-
72
- def push
73
- status = true
74
- tags.each do |tag|
75
- logger.info "Tagging #{image_id} as #{layer}:#{tag}"
76
- retval = runner.run("docker tag #{image_id} #{project}:#{tag}")
77
- next unless retval.zero?
78
- retval = runner.run("docker push #{project}:#{tag}")
79
-
80
- status &= retval.zero?
81
- end
82
-
83
- tags if status
84
- end
85
- end
86
- end