blubber 0.2.0 → 0.3.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
  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