fpm-fry 0.2.2 → 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
  SHA1:
3
- metadata.gz: 30f03302993885946a899feba8c6f6a63f9cc46a
4
- data.tar.gz: 6cf9d78c60aca34c1c8feab1b58ddd9234fe28b9
3
+ metadata.gz: de6bc3a9f29acd7c147a4016c4906fd0a35c6481
4
+ data.tar.gz: afece6f8aea44147df5e357015115f9c74614071
5
5
  SHA512:
6
- metadata.gz: d832baed68a8f809a009bcdb7d8addb4847393475f1bc4ae12d5a562210ddbc3506f1e6e310d4798b68197b75a649152ea05449ce2ef18ad4e34f321094efb9d
7
- data.tar.gz: ca77a79b3aad02271b8dd9cf4e9c31d556c7125065ea7b1df0917d3677a433b64b8a32a7bd90e3a527a5f7016533dcd766393472cf5c30cb8b4d99e16fb6107f
6
+ metadata.gz: 3fea3ee2114b957eca0789a06faab401d88ff9269bf95ffab42ce79f367716d4b962b964e44aa03ef58087c50c1df5c904195f1ac44cde705bc17c5c0a9d2e63
7
+ data.tar.gz: ccee1a75f08a186ff5a44e673d297ca41e2ce6faee2b4df57715542eafbf8640b605e0f7c619958f8b4a7967abd76b8192eadd6e72d5ffcecb07ab1ca27c9949
@@ -80,8 +80,23 @@ class Cabin::NiceOutput
80
80
  @io.flush
81
81
  end
82
82
 
83
+ private
84
+
83
85
  def pp(hash)
84
- hash.map{|k,v| ' '+k.to_s + ": " + v.inspect }.join("\n")
86
+ hash.map{|k,v| ' '+k.to_s + ": " + pp_value(v) }.join("\n")
87
+ end
88
+
89
+ def pp_value(value)
90
+ case(value)
91
+ when String
92
+ if value.include? "\n"
93
+ return "\n\t" + value.gsub("\n","\n\t")
94
+ else
95
+ return value
96
+ end
97
+ else
98
+ return value.inspect
99
+ end
85
100
  end
86
101
 
87
102
  end
@@ -1,10 +1,13 @@
1
1
  module FPM; module Fry
2
+ # Helper class that reads an IO in chunks.
3
+ #
4
+ # @api private
2
5
  class BlockEnumerator < Struct.new(:io, :blocksize)
3
6
  include Enumerable
4
7
 
5
- # @param io [IO]
6
- # @param blocksize [Numeric]
7
- def initialize(_, blocksize = 128)
8
+ # @param [IO] io
9
+ # @param [Numeric] blocksize
10
+ def initialize(io, blocksize = 128)
8
11
  super
9
12
  end
10
13
 
@@ -1,17 +1,30 @@
1
1
  require 'cabin/channel'
2
2
  module FPM; module Fry
3
+ # A {Cabin::Channel} with two additional features:
4
+ #
5
+ # - There is a new log level 'hint' which can point users to improvements.
6
+ # - Logging an Exception that responds to #data will merge in the data from
7
+ # this exception. This is used together with {FPM::Fry::WithData}
8
+ #
9
+ # @api internal
3
10
  class Channel < Cabin::Channel
4
11
 
5
12
  module Hint
13
+ # Logs a message with level 'hint'
14
+ #
15
+ # @param [String] message
16
+ # @param [Hash] data
6
17
  def hint( message, data = {} )
7
18
  return unless hint?
8
19
  log(message, data.merge(level: :hint))
9
20
  end
10
21
 
22
+ # True if hints should be displayed
11
23
  def hint?
12
24
  !defined?(@hint) || @hint
13
25
  end
14
26
 
27
+ # Switched hints on or off
15
28
  def hint=( bool )
16
29
  @hint = !!bool
17
30
  end
@@ -14,7 +14,7 @@ module FPM ; module Fry
14
14
 
15
15
  # Returns all directory entries like Dir.entries.
16
16
  # @param [String] path
17
- # @result [Array<String>] entries
17
+ # @return [Array<String>] entries
18
18
  def entries(path)
19
19
  dir = rebase(path)
20
20
  return Dir.entries(dir)
@@ -34,7 +34,7 @@ module FPM ; module Fry
34
34
 
35
35
  # Yields all entries recursively like Find.find.
36
36
  # @param [String] path
37
- # @yields entry
37
+ # @yield entry
38
38
  # @yieldparam [String] entry
39
39
  def find(path, &block)
40
40
  if stat(path).directory?
@@ -1,15 +1,27 @@
1
+ require 'cabin'
1
2
  require 'excon'
2
3
  require 'rubygems/package'
3
4
  require 'json'
4
5
  require 'fileutils'
5
6
  require 'forwardable'
6
7
  require 'fpm/fry/tar'
7
-
8
- module FPM; module Fry; end ; end
9
-
8
+ require 'fpm/fry/with_data'
10
9
  class FPM::Fry::Client
11
10
 
11
+ # Raised when a file wasn't found inside a container
12
12
  class FileNotFound < StandardError
13
+ include FPM::Fry::WithData
14
+ end
15
+
16
+ # Raised when a container wasn't found.
17
+ class ContainerNotFound < StandardError
18
+ include FPM::Fry::WithData
19
+ end
20
+
21
+ # Raised when trying to read file that can't be read e.g. because it's a
22
+ # directory.
23
+ class NotAFile < StandardError
24
+ include FPM::Fry::WithData
13
25
  end
14
26
 
15
27
  extend Forwardable
@@ -41,6 +53,7 @@ class FPM::Fry::Client
41
53
  end
42
54
  end
43
55
 
56
+ # @return [String] docker server api version
44
57
  def server_version
45
58
  @server_version ||= begin
46
59
  res = agent.get(
@@ -51,6 +64,7 @@ class FPM::Fry::Client
51
64
  end
52
65
  end
53
66
 
67
+ # @return [String] docker cert path from environment
54
68
  def self.docker_cert_path
55
69
  ENV.fetch('DOCKER_CERT_PATH',File.join(Dir.home, '.docker'))
56
70
  end
@@ -72,12 +86,17 @@ class FPM::Fry::Client
72
86
  return to_enum(:read, name, resource) unless block_given?
73
87
  res = agent.get(
74
88
  path: url('containers',name,'archive'),
75
- query: URI.encode_www_form('path' => resource),
89
+ query: {'path' => resource},
76
90
  headers: { 'Content-Type' => 'application/json' },
77
91
  expects: [200,404,500]
78
92
  )
79
- if res.status == 500
80
- raise FileNotFound, "File #{resource.inspect} not found: #{res.body}"
93
+ if [404,500].include? res.status
94
+ body_message = Hash[JSON.load(res.body).map{|k,v| ["docker.#{k}",v] }] rescue {'docker.message' => res.body}
95
+ body_message['docker.container'] = name
96
+ if body_message['docker.message'] =~ /\ANo such container:/
97
+ raise ContainerNotFound.new("container not found", body_message)
98
+ end
99
+ raise FileNotFound.new("file not found", {'path' => resource}.merge(body_message))
81
100
  end
82
101
  sio = StringIO.new(res.body)
83
102
  tar = ::Gem::Package::TarReader.new( sio )
@@ -86,6 +105,41 @@ class FPM::Fry::Client
86
105
  end
87
106
  end
88
107
 
108
+ # Gets the file contents while following symlinks
109
+ # @param [String] name the container name
110
+ # @param [String] resource the file name
111
+ # @return [String] content
112
+ # @raise [NotAFile] when the file has no readable content
113
+ # @raise [FileNotFound] when the file does not exist
114
+ # @api docker
115
+ def read_content(name, resource)
116
+ read(name, resource) do |file|
117
+ if file.header.typeflag == "2"
118
+ return read_content(name, File.absolute_path(file.header.linkname,File.dirname(resource)))
119
+ end
120
+ if file.header.typeflag != "0"
121
+ raise NotAFile.new("not a file", {'path' => resource})
122
+ end
123
+ return file.read
124
+ end
125
+ end
126
+
127
+ # Gets the target of a symlink
128
+ # @param [String] name the container name
129
+ # @param [String] resource the file name
130
+ # @return [String] target
131
+ # @return [nil] if resource is not a symlink
132
+ # @api docker
133
+ def link_target(name, resource)
134
+ read(name, resource) do |file|
135
+ if file.header.typeflag == "2"
136
+ return File.absolute_path(file.header.linkname,File.dirname(resource))
137
+ end
138
+ return nil
139
+ end
140
+ return nil
141
+ end
142
+
89
143
  def copy(name, resource, map, options = {})
90
144
  ex = FPM::Fry::Tar::Extractor.new(logger: @logger)
91
145
  base = File.dirname(resource)
@@ -105,6 +159,27 @@ class FPM::Fry::Client
105
159
  return JSON.parse(res.body)
106
160
  end
107
161
 
162
+ def pull(image)
163
+ agent.post(path: url('images','create'), query: {'fromImage' => image})
164
+ end
165
+
166
+ def create(image)
167
+ res = agent.post(
168
+ headers: { 'Content-Type' => 'application/json' },
169
+ path: url('containers','create'),
170
+ expects: [201],
171
+ body: JSON.generate('Image' => image)
172
+ )
173
+ return JSON.parse(res.body)['Id']
174
+ end
175
+
176
+ def destroy(container)
177
+ agent.delete(
178
+ path: url('containers',container),
179
+ expects: [204]
180
+ )
181
+ end
182
+
108
183
  def agent
109
184
  @agent ||= agent_for(docker_url, tls)
110
185
  end
@@ -23,6 +23,7 @@ module FPM; module Fry
23
23
  def initialize(invocation_path, ctx = {}, parent_attribute_values = {})
24
24
  super
25
25
  @ui = ctx.fetch(:ui){ UI.new }
26
+ @client = ctx[:client]
26
27
  end
27
28
 
28
29
  def parse(attrs)
@@ -45,48 +46,27 @@ module FPM; module Fry
45
46
 
46
47
  attr_writer :client
47
48
 
48
- subcommand 'detect', 'Detects distribution from an image, a container or a given name' do
49
+ subcommand 'detect', 'Detects distribution from an image' do
49
50
 
50
- option '--image', 'image', 'Docker image to detect'
51
- option '--container', 'container', 'Docker container to detect'
52
- option '--distribution', 'distribution', 'Distribution name to detect'
51
+ parameter 'image', 'Docker image to detect'
53
52
 
54
53
  attr :ui
55
54
  extend Forwardable
56
55
  def_delegators :ui, :logger
57
56
 
58
57
  def execute
59
- require 'fpm/fry/os_db'
58
+ require 'fpm/fry/inspector'
60
59
  require 'fpm/fry/detector'
61
60
 
62
- if image
63
- d = Detector::Image.new(client, image)
64
- elsif distribution
65
- d = Detector::String.new(distribution)
66
- elsif container
67
- d = Detector::Container.new(client, container)
68
- else
69
- logger.error("Please supply either --image, --distribution or --container")
70
- return 1
71
- end
72
-
73
- begin
74
- if d.detect!
75
- data = {distribution: d.distribution, version: d.version}
76
- if i = OsDb[d.distribution]
77
- data[:flavour] = i[:flavour]
78
- else
79
- data[:flavour] = "unknown"
80
- end
81
- logger.info("Detected distribution",data)
61
+ Inspector.for_image(client, image) do | inspector |
62
+ begin
63
+ data = Detector.detect(inspector)
64
+ logger.info("Detected the following parameters",data)
82
65
  return 0
83
- else
84
- logger.error("Detection failed")
85
- return 2
66
+ rescue => e
67
+ logger.error(e)
68
+ return 1
86
69
  end
87
- rescue => e
88
- logger.error(e)
89
- return 3
90
70
  end
91
71
  end
92
72
 
@@ -2,7 +2,6 @@ require 'fpm/fry/command'
2
2
  module FPM; module Fry
3
3
  class Command::Cook < Command
4
4
 
5
- option '--distribution', 'distribution', 'Distribution like ubuntu-12.04'
6
5
  option '--keep', :flag, 'Keep the container after build'
7
6
  option '--overwrite', :flag, 'Overwrite package', default: true
8
7
 
@@ -22,40 +21,19 @@ module FPM; module Fry
22
21
  @tls = nil
23
22
  require 'digest'
24
23
  require 'fileutils'
24
+ require 'fpm/fry/with_data'
25
25
  require 'fpm/fry/recipe'
26
26
  require 'fpm/fry/recipe/builder'
27
27
  require 'fpm/fry/detector'
28
28
  require 'fpm/fry/docker_file'
29
29
  require 'fpm/fry/stream_parser'
30
- require 'fpm/fry/os_db'
31
30
  require 'fpm/fry/block_enumerator'
32
31
  require 'fpm/fry/build_output_parser'
32
+ require 'fpm/fry/inspector'
33
+ require 'fpm/fry/plugin/config'
33
34
  super
34
35
  end
35
36
 
36
- def detector
37
- @detector || begin
38
- if distribution
39
- d = Detector::String.new(distribution)
40
- else
41
- d = Detector::Image.new(client, image)
42
- end
43
- self.detector=d
44
- end
45
- end
46
-
47
- def detector=(d)
48
- unless d.detect!
49
- raise "Unable to detect distribution from given image"
50
- end
51
- @detector = d
52
- end
53
-
54
- def flavour
55
- @flavour ||= OsDb.fetch(detector.distribution,{flavour: "unknown"})[:flavour]
56
- end
57
- attr_writer :flavour
58
-
59
37
  def output_class
60
38
  @output_class ||= begin
61
39
  logger.debug("Autodetecting package type",flavour: flavour)
@@ -75,19 +53,22 @@ module FPM; module Fry
75
53
 
76
54
  def builder
77
55
  @builder ||= begin
78
- vars = {
79
- distribution: detector.distribution,
80
- distribution_version: detector.version,
81
- flavour: flavour
82
- }
83
- logger.debug("Loading recipe",variables: vars, recipe: recipe)
84
- b = Recipe::Builder.new(vars, Recipe.new, logger: ui.logger)
85
- b.load_file( recipe )
56
+ b = nil
57
+ Inspector.for_image(client, image) do |inspector|
58
+ variables = Detector.detect(inspector)
59
+ logger.debug("Loading recipe",variables: variables, recipe: recipe)
60
+ b = Recipe::Builder.new(variables, logger: ui.logger, inspector: inspector)
61
+ b.load_file( recipe )
62
+ end
86
63
  b
87
64
  end
88
65
  end
89
66
  attr_writer :builder
90
67
 
68
+ def flavour
69
+ builder.variables[:flavour]
70
+ end
71
+
91
72
  def cache
92
73
  @cache ||= builder.recipe.source.build_cache(tmpdir)
93
74
  end
@@ -168,22 +149,12 @@ module FPM; module Fry
168
149
  if flavour == 'debian'
169
150
  case(update)
170
151
  when 'auto'
171
- body = JSON.generate({"Image" => image, "Cmd" => "exit 0"})
172
- res = client.post( path: client.url('containers','create'),
173
- headers: {'Content-Type' => 'application/json'},
174
- body: body,
175
- expects: [201]
176
- )
177
- body = JSON.parse(res.body)
178
- container = body.fetch('Id')
179
- begin
180
- client.read( container, '/var/lib/apt/lists') do |file|
152
+ Inspector.for_image(client, image) do |inspector|
153
+ inspector.read('/var/lib/apt/lists') do |file|
181
154
  next if file.header.name == 'lists/'
182
155
  logger.hint("/var/lib/apt/lists is not empty, you could try to speed up builds with --update=never", documentation: 'https://github.com/xing/fpm-fry/wiki/The-update-parameter')
183
- return true
156
+ break
184
157
  end
185
- ensure
186
- client.delete(path: client.url('containers',container))
187
158
  end
188
159
  return true
189
160
  when 'always'
@@ -242,7 +213,7 @@ module FPM; module Fry
242
213
  return yield container
243
214
  ensure
244
215
  unless keep?
245
- client.delete(path: client.url('containers',container))
216
+ client.destroy(container)
246
217
  end
247
218
  end
248
219
  end
@@ -359,11 +330,8 @@ module FPM; module Fry
359
330
  def execute
360
331
  # force some eager loading
361
332
  lint_recipe_file!
362
- detector
363
- flavour
364
- output_class
365
- lint_output_class!
366
333
  builder
334
+ lint_output_class!
367
335
  lint_recipe!
368
336
  cache
369
337
 
@@ -1,117 +1,62 @@
1
- require 'fpm/fry/client'
1
+ require 'fpm/fry/detector'
2
2
  module FPM; module Fry
3
3
 
4
4
  module Detector
5
-
6
- class String < Struct.new(:value)
7
- attr :distribution
8
- attr :version
9
-
10
- def detect!
11
- @distribution, @version = value.split('-',2)
12
- return true
5
+ # Detects a set of basic properties about an image.
6
+ #
7
+ # @param [Inspector] inspector
8
+ # @return [Hash<Symbol, String>]
9
+ def self.detect(inspector)
10
+ found = {}
11
+ if inspector.exists? '/usr/bin/apt-get'
12
+ found[:flavour] = 'debian'
13
+ elsif inspector.exists? '/bin/rpm'
14
+ found[:flavour] = 'redhat'
13
15
  end
14
-
15
- end
16
-
17
- class Container < Struct.new(:client,:container)
18
- attr :distribution
19
- attr :version
20
-
21
- def detect!
22
- begin
23
- client.read(container,'/etc/lsb-release') do |file|
24
- file.read.each_line do |line|
25
- case(line)
26
- when /\ADISTRIB_ID=/ then
27
- @distribution = $'.strip.downcase
28
- when /\ADISTRIB_RELEASE=/ then
29
- @version = $'.strip
30
- end
31
- end
32
- end
33
- return !!(@distribution and @version)
34
- rescue Client::FileNotFound
35
- end
36
- begin
37
- client.read(container,'/etc/debian_version') do |file|
38
- content = file.read
39
- if /\A\d+(?:\.\d+)+\Z/ =~ content
40
- @distribution = 'debian'
41
- @version = content.strip
42
- end
43
- end
44
- return !!(@distribution and @version)
45
- rescue Client::FileNotFound
46
- end
47
- begin
48
- client.read(container,'/etc/redhat-release') do |file|
49
- if file.header.typeflag == "2" # centos links this file
50
- client.read(container,File.absolute_path(file.header.linkname,'/etc')) do |file|
51
- detect_redhat_release(file)
52
- end
53
- else
54
- detect_redhat_release(file)
55
- end
16
+ begin
17
+ inspector.read_content('/etc/lsb-release').each_line do |line|
18
+ case(line)
19
+ when /\ADISTRIB_ID=/ then
20
+ found[:distribution] = $'.strip.downcase
21
+ when /\ADISTRIB_RELEASE=/ then
22
+ found[:release] = $'.strip
23
+ when /\ADISTRIB_CODENAME=/ then
24
+ found[:codename] = $'.strip
56
25
  end
57
- return !!(@distribution and @version)
58
- rescue Client::FileNotFound
59
26
  end
60
- return false
27
+ rescue Client::FileNotFound
61
28
  end
62
29
 
63
- private
64
- def detect_redhat_release(file)
65
- file.read.each_line do |line|
30
+ begin
31
+ inspector.read_content('/etc/os-release').each_line do |line|
66
32
  case(line)
67
- when /\A(\w+) release ([\d\.]+)/ then
68
- @distribution = $1.strip.downcase
69
- @version = $2.strip
33
+ when /\AVERSION=\"(\w+) \((\w+)\)\"/ then
34
+ found[:release] ||= $1
35
+ found[:codename] ||= $2
70
36
  end
71
37
  end
38
+ rescue Client::FileNotFound
72
39
  end
73
- end
74
-
75
- class Image < Struct.new(:client,:image,:factory)
76
-
77
- class ImageNotFound < StandardError
78
- end
79
-
80
- attr :distribution
81
- attr :version
82
-
83
- def initialize(client, image, factory = Container)
84
- super
85
- end
86
-
87
- def detect!
88
- body = JSON.generate({"Image" => image, "Cmd" => "exit 0"})
89
- begin
90
- res = client.post( path: client.url('containers','create'),
91
- headers: {'Content-Type' => 'application/json'},
92
- body: body,
93
- expects: [201]
94
- )
95
- rescue Excon::Errors::NotFound
96
- raise ImageNotFound, "Image #{image.inspect} not found. Did you do a `docker pull #{image}` before?"
40
+ begin
41
+ content = inspector.read_content('/etc/debian_version')
42
+ if /\A\d+(?:\.\d+)+\Z/ =~ content
43
+ found[:distribution] ||= 'debian'
44
+ found[:release] = content.strip
97
45
  end
98
- body = JSON.parse(res.body)
99
- container = body.fetch('Id')
100
- begin
101
- d = factory.new(client,container)
102
- if d.detect!
103
- @distribution = d.distribution
104
- @version = d.version
105
- return true
106
- else
107
- return false
46
+ rescue Client::FileNotFound
47
+ end
48
+ begin
49
+ content = inspector.read_content('/etc/redhat-release')
50
+ content.each_line do |line|
51
+ case(line)
52
+ when /\A(\w+)(?: Linux)? release ([\d\.]+)/ then
53
+ found[:distribution] ||= $1.strip.downcase
54
+ found[:release] = $2.strip
108
55
  end
109
- ensure
110
- client.delete(path: client.url('containers',container))
111
56
  end
57
+ rescue Client::FileNotFound
112
58
  end
59
+ return found
113
60
  end
114
-
115
-
116
61
  end
117
62
  end ; end