fpm-fry 0.2.2 → 0.4.6

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.
@@ -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
@@ -1,7 +1,6 @@
1
1
  require 'fiber'
2
2
  require 'shellwords'
3
3
  require 'rubygems/package'
4
- require 'fpm/fry/os_db'
5
4
  require 'fpm/fry/source'
6
5
  require 'fpm/fry/joined_io'
7
6
  module FPM; module Fry
@@ -12,12 +11,13 @@ module FPM; module Fry
12
11
  class Source < Struct.new(:variables, :cache)
13
12
 
14
13
  def initialize(variables, cache = Source::Null::Cache)
15
- variables = variables.dup
16
- if variables[:distribution] && !variables[:flavour] && OsDb[variables[:distribution]]
17
- variables[:flavour] = OsDb[variables[:distribution]][:flavour]
18
- end
19
- variables.freeze
14
+ variables = variables.dup.freeze
20
15
  super(variables, cache)
16
+ if cache.respond_to? :logger
17
+ @logger = cache.logger
18
+ else
19
+ @logger = Cabin::Channel.get
20
+ end
21
21
  end
22
22
 
23
23
  def dockerfile
@@ -26,8 +26,8 @@ module FPM; module Fry
26
26
 
27
27
  df << "RUN mkdir /tmp/build"
28
28
 
29
- cache.file_map.each do |from, to|
30
- df << "ADD #{map_from(from)} #{map_to(to)}"
29
+ file_map.each do |from, to|
30
+ df << "COPY #{map_from(from)} #{map_to(to)}"
31
31
  end
32
32
 
33
33
  df << ""
@@ -52,6 +52,33 @@ module FPM; module Fry
52
52
  return sio
53
53
  end
54
54
 
55
+ private
56
+
57
+ attr :logger
58
+
59
+ def file_map
60
+ prefix = ""
61
+ to = ""
62
+ if cache.respond_to? :prefix
63
+ prefix = cache.prefix
64
+ end
65
+ if cache.respond_to? :to
66
+ to = cache.to || ""
67
+ end
68
+ fm = cache.file_map
69
+ if fm.nil?
70
+ return { prefix => to }
71
+ end
72
+ if fm.size == 1
73
+ key, value = fm.first
74
+ key = key.gsub(%r!\A\./|/\z!,'')
75
+ if ["",".","./"].include?(value) && key == prefix
76
+ logger.hint("You can remove the file_map: #{fm.inspect} option on source. The given value is the default")
77
+ end
78
+ end
79
+ return fm
80
+ end
81
+
55
82
  def map_to(dir)
56
83
  if ['','.'].include? dir
57
84
  return '/tmp/build'
@@ -76,27 +103,34 @@ module FPM; module Fry
76
103
  private :options
77
104
 
78
105
  def initialize(base, variables, recipe, options = {})
79
- variables = variables.dup
80
- if variables[:distribution] && !variables[:flavour] && OsDb[variables[:distribution]]
81
- variables[:flavour] = OsDb[variables[:distribution]][:flavour]
82
- end
83
- variables.freeze
106
+ variables = variables.dup.freeze
107
+ raise Fry::WithData('unknown flavour', 'flavour' => variables[:flavour]) unless ['debian','redhat'].include? variables[:flavour]
84
108
  @options = options.dup.freeze
85
109
  super(base, variables, recipe)
86
110
  end
87
111
 
88
112
  def dockerfile
89
- df = []
90
- df << "FROM #{base}"
91
- df << "WORKDIR /tmp/build"
113
+ df = {
114
+ source: [],
115
+ dependencies: [],
116
+ build: []
117
+ }
118
+ df[:source] << "FROM #{base}"
119
+ workdir = '/tmp/build'
120
+ # TODO: get this from cache, not from the source itself
121
+ if recipe.source.respond_to? :to
122
+ to = recipe.source.to || ""
123
+ workdir = File.expand_path(to, workdir)
124
+ end
125
+ df[:source] << "WORKDIR #{workdir}"
92
126
 
93
127
  # need to add external sources before running any command
94
128
  recipe.build_mounts.each do |source, target|
95
- df << "ADD #{source} /tmp/build/#{target}"
129
+ df[:dependencies] << "COPY #{source} ./#{target}"
96
130
  end
97
131
 
98
- recipe.apt_setup.each do |step|
99
- df << "RUN #{step}"
132
+ recipe.before_dependencies_steps.each do |step|
133
+ df[:dependencies] << "RUN #{step.to_s}"
100
134
  end
101
135
 
102
136
  if build_dependencies.any?
@@ -106,22 +140,22 @@ module FPM; module Fry
106
140
  if options[:update]
107
141
  update = 'apt-get update && '
108
142
  end
109
- df << "RUN #{update}apt-get install --yes #{Shellwords.join(build_dependencies)}"
143
+ df[:dependencies] << "RUN #{update}apt-get install --yes #{Shellwords.join(build_dependencies)}"
110
144
  when 'redhat'
111
- df << "RUN yum -y install #{Shellwords.join(build_dependencies)}"
145
+ df[:dependencies] << "RUN yum -y install #{Shellwords.join(build_dependencies)}"
112
146
  else
113
147
  raise "Unknown flavour: #{variables[:flavour]}"
114
148
  end
115
149
  end
116
150
 
117
151
  recipe.before_build_steps.each do |step|
118
- df << "RUN #{step.to_s}"
152
+ df[:build] << "RUN #{step.to_s}"
119
153
  end
120
154
 
121
- df << "ADD .build.sh /tmp/build/"
122
- df << "ENTRYPOINT /tmp/build/.build.sh"
123
- df << ''
124
- return df.join("\n")
155
+ df[:build] << "COPY .build.sh #{workdir}/"
156
+ df[:build] << "CMD #{workdir}/.build.sh"
157
+ recipe.apply_dockerfile_hooks(df)
158
+ return [*df[:source],*df[:dependencies],*df[:build],""].join("\n")
125
159
  end
126
160
 
127
161
  def build_sh
@@ -0,0 +1,76 @@
1
+ require 'fpm/fry/with_data'
2
+ require 'open3'
3
+ module FPM
4
+ module Fry
5
+
6
+ module Exec
7
+
8
+ # Raised when running a command failed.
9
+ class Failed < StandardError
10
+ include WithData
11
+
12
+ # @return [String] contents of stderr
13
+ def stderr
14
+ data[:stderr]
15
+ end
16
+
17
+ end
18
+
19
+ class << self
20
+
21
+ # @!method [](*cmd, options = {}) Runs a command and returns its stdout as string. This method is preferred if the expected output is short.
22
+ # @param [Array<String>] cmd command to run
23
+ # @param [Hash] options
24
+ # @option options [Cabin::Channel] :logger
25
+ # @option options [String] :description human readable string to describe what the command is doing
26
+ # @option options [String] :stdin_data data to write to stding
27
+ # @option options [String] :chdir directory to change to
28
+ # @return [String] stdout
29
+ # @raise [FPM::Fry::Exec::Failed] when exitcode != 0
30
+ def [](*args)
31
+ cmd, options, description = extract_options_and_log(args)
32
+ stdout, stderr, status = Open3.capture3(*cmd, options)
33
+ if status.exitstatus != 0
34
+ raise Exec.const_get("ExitCode#{status.exitstatus}").new("#{description} failed", exitstatus: status.exitstatus, stderr: stderr, stdout: stdout, command: cmd)
35
+ end
36
+ return stdout
37
+ end
38
+
39
+ alias exec []
40
+
41
+ # @!method popen(*cmd, options = {}) Runs a command and returns its stdout as IO.
42
+ # @param [Array<String>] cmd command to run
43
+ # @param [Hash] options
44
+ # @option options [Cabin::Channel] :logger
45
+ # @option options [String] :description human readable string to describe what the command is doing
46
+ # @option options [String] :chdir directory to change to
47
+ # @return [IO] stdout
48
+ def popen(*args)
49
+ cmd, options, _description = extract_options_and_log(args)
50
+ return IO.popen(cmd, options)
51
+ end
52
+ private
53
+ def extract_options_and_log(args)
54
+ options = args.last.kind_of?(Hash) ? args.pop.dup : {}
55
+ cmd = args
56
+ logger = options.delete(:logger)
57
+ description = options.delete(:description) || "Running #{cmd.join(' ')}"
58
+ if logger
59
+ logger.debug(description, command: args)
60
+ end
61
+ return cmd, options, description
62
+ end
63
+
64
+ def const_missing(name)
65
+ if name.to_s =~ /\AExitCode\d+\z/
66
+ klass = Class.new(Failed)
67
+ const_set(name, klass)
68
+ return klass
69
+ end
70
+ super
71
+ end
72
+
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,70 @@
1
+ module FPM::Fry
2
+ # An inspector allows a plugin to gather information about the image used to
3
+ # build a package.
4
+ class Inspector
5
+
6
+ # Gets the file content at path.
7
+ #
8
+ # @param [String] path path to a file
9
+ # @raise [FPM::Fry::Client::FileNotFound] when the given path doesn't exist
10
+ # @raise [FPM::Fry::Client::NotAFile] when the given path is not a file
11
+ # @return [String] file content as string
12
+ def read_content(path)
13
+ return client.read_content(container, path)
14
+ end
15
+
16
+ # Gets whatever is at path. This once if path is a file. And all subfiles
17
+ # if it's a directory. Usually read_content is better.
18
+ #
19
+ # @param [String] path path to a file
20
+ # @raise [FPM::Fry::Client::FileNotFound] when the given path doesn't exist
21
+ # @raise [FPM::Fry::Client::NotAFile] when the given path is not a file
22
+ # @yield [entry] tar file entry
23
+ # @yieldparam entry [Gem::Package::TarEntry]
24
+ def read(path, &block)
25
+ return client.read(container, path, &block)
26
+ end
27
+
28
+ # Determines the target of a link
29
+ #
30
+ # @param [String] path
31
+ # @raise [FPM::Fry::Client::FileNotFound] when the given path doesn't exist
32
+ # @return [String] target
33
+ # @return [nil] when file is not a link
34
+ def link_target(path)
35
+ return client.link_target(container, path)
36
+ end
37
+
38
+ # Checks if file exists at path
39
+ #
40
+ # @param [String] path
41
+ # @return [true] when path exists
42
+ # @return [false] otherwise
43
+ def exists?(path)
44
+ client.read(container,path) do
45
+ return true
46
+ end
47
+ rescue FPM::Fry::Client::FileNotFound
48
+ return false
49
+ end
50
+
51
+ def self.for_image(client, image)
52
+ container = client.create(image)
53
+ begin
54
+ yield new(client, container)
55
+ ensure
56
+ client.destroy(container)
57
+ end
58
+ end
59
+
60
+ private
61
+ def initialize(client, container)
62
+ @client, @container = client, container
63
+ end
64
+
65
+ attr :client
66
+ attr :container
67
+
68
+ end
69
+
70
+ end