fpm-fry 0.2.2 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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