fpm-fry 0.1.3

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/bin/fpm-fry +10 -0
  3. data/lib/cabin/nice_output.rb +70 -0
  4. data/lib/fpm/fry/block_enumerator.rb +25 -0
  5. data/lib/fpm/fry/build_output_parser.rb +22 -0
  6. data/lib/fpm/fry/client.rb +162 -0
  7. data/lib/fpm/fry/command/cook.rb +370 -0
  8. data/lib/fpm/fry/command.rb +90 -0
  9. data/lib/fpm/fry/detector.rb +109 -0
  10. data/lib/fpm/fry/docker_file.rb +149 -0
  11. data/lib/fpm/fry/joined_io.rb +63 -0
  12. data/lib/fpm/fry/os_db.rb +35 -0
  13. data/lib/fpm/fry/plugin/alternatives.rb +90 -0
  14. data/lib/fpm/fry/plugin/edit_staging.rb +66 -0
  15. data/lib/fpm/fry/plugin/exclude.rb +18 -0
  16. data/lib/fpm/fry/plugin/init.rb +53 -0
  17. data/lib/fpm/fry/plugin/platforms.rb +10 -0
  18. data/lib/fpm/fry/plugin/script_helper.rb +176 -0
  19. data/lib/fpm/fry/plugin/service.rb +100 -0
  20. data/lib/fpm/fry/plugin.rb +3 -0
  21. data/lib/fpm/fry/recipe/builder.rb +267 -0
  22. data/lib/fpm/fry/recipe.rb +141 -0
  23. data/lib/fpm/fry/source/dir.rb +56 -0
  24. data/lib/fpm/fry/source/git.rb +90 -0
  25. data/lib/fpm/fry/source/package.rb +202 -0
  26. data/lib/fpm/fry/source/patched.rb +118 -0
  27. data/lib/fpm/fry/source.rb +47 -0
  28. data/lib/fpm/fry/stream_parser.rb +98 -0
  29. data/lib/fpm/fry/tar.rb +71 -0
  30. data/lib/fpm/fry/templates/debian/after_install.erb +9 -0
  31. data/lib/fpm/fry/templates/debian/before_install.erb +13 -0
  32. data/lib/fpm/fry/templates/debian/before_remove.erb +13 -0
  33. data/lib/fpm/fry/templates/redhat/after_install.erb +2 -0
  34. data/lib/fpm/fry/templates/redhat/before_install.erb +6 -0
  35. data/lib/fpm/fry/templates/redhat/before_remove.erb +6 -0
  36. data/lib/fpm/fry/templates/sysv.erb +125 -0
  37. data/lib/fpm/fry/templates/upstart.erb +15 -0
  38. data/lib/fpm/fry/ui.rb +12 -0
  39. data/lib/fpm/package/docker.rb +186 -0
  40. metadata +111 -0
@@ -0,0 +1,90 @@
1
+ require 'tmpdir'
2
+ require 'fileutils'
3
+ require 'clamp'
4
+ require 'json'
5
+ require 'forwardable'
6
+ require 'fpm/fry/ui'
7
+ require 'fpm/command'
8
+
9
+ module FPM; module Fry
10
+
11
+ class Command < Clamp::Command
12
+
13
+ option '--debug', :flag, 'Turns on debugging'
14
+ option '--[no-]tls', :flag, 'Turns on tls ( default is false for schema unix, tcp and http and true for https )'
15
+ option '--[no-]tlsverify', :flag, 'Turns off tls peer verification', default:true, environment_variable: 'DOCKER_TLS_VERIFY'
16
+
17
+ subcommand 'fpm', 'Works like fpm but with docker support', FPM::Command
18
+
19
+ def client
20
+ @client ||= begin
21
+ client = FPM::Fry::Client.new(
22
+ logger: logger,
23
+ tls: tls?, tlsverify: tlsverify?
24
+ )
25
+ logger.info("Docker connected",client.server_version)
26
+ client
27
+ end
28
+ end
29
+
30
+ attr_writer :client
31
+
32
+ subcommand 'detect', 'Detects distribution from an image, a container or a given name' do
33
+
34
+ option '--image', 'image', 'Docker image to detect'
35
+ option '--container', 'container', 'Docker container to detect'
36
+ option '--distribution', 'distribution', 'Distribution name to detect'
37
+
38
+ attr :ui
39
+ extend Forwardable
40
+ def_delegators :ui, :logger
41
+
42
+ def initialize(*_)
43
+ super
44
+ @ui = UI.new()
45
+ if debug?
46
+ ui.logger.level = :debug
47
+ end
48
+ end
49
+
50
+ def execute
51
+ require 'fpm/fry/os_db'
52
+ require 'fpm/fry/detector'
53
+
54
+ if image
55
+ d = Detector::Image.new(client, image)
56
+ elsif distribution
57
+ d = Detector::String.new(distribution)
58
+ elsif container
59
+ d = Detector::Container.new(client, container)
60
+ else
61
+ logger.error("Please supply either --image, --distribution or --container")
62
+ return 1
63
+ end
64
+
65
+ begin
66
+ if d.detect!
67
+ data = {distribution: d.distribution, version: d.version}
68
+ if i = OsDb[d.distribution]
69
+ data[:flavour] = i[:flavour]
70
+ else
71
+ data[:flavour] = "unknown"
72
+ end
73
+ logger.info("Detected distribution",data)
74
+ return 0
75
+ else
76
+ logger.error("Detection failed")
77
+ return 2
78
+ end
79
+ rescue => e
80
+ logger.error(e)
81
+ return 3
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+
88
+ end ; end
89
+
90
+ require 'fpm/fry/command/cook'
@@ -0,0 +1,109 @@
1
+ require 'fpm/fry/client'
2
+ module FPM; module Fry
3
+
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
13
+ 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
56
+ end
57
+ return !!(@distribution and @version)
58
+ rescue Client::FileNotFound
59
+ end
60
+ return false
61
+ end
62
+
63
+ private
64
+ def detect_redhat_release(file)
65
+ file.read.each_line do |line|
66
+ case(line)
67
+ when /\A(\w+) release ([\d\.]+)/ then
68
+ @distribution = $1.strip.downcase
69
+ @version = $2.strip
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ class Image < Struct.new(:client,:image,:factory)
76
+ attr :distribution
77
+ attr :version
78
+
79
+ def initialize(client, image, factory = Container)
80
+ super
81
+ end
82
+
83
+ def detect!
84
+ body = JSON.generate({"Image" => image, "Cmd" => "exit 0"})
85
+ res = client.post( path: client.url('containers','create'),
86
+ headers: {'Content-Type' => 'application/json'},
87
+ body: body,
88
+ expects: [201]
89
+ )
90
+ body = JSON.parse(res.body)
91
+ container = body.fetch('Id')
92
+ begin
93
+ d = factory.new(client,container)
94
+ if d.detect!
95
+ @distribution = d.distribution
96
+ @version = d.version
97
+ return true
98
+ else
99
+ return false
100
+ end
101
+ ensure
102
+ client.delete(path: client.url('containers',container))
103
+ end
104
+ end
105
+ end
106
+
107
+
108
+ end
109
+ end ; end
@@ -0,0 +1,149 @@
1
+ require 'fiber'
2
+ require 'shellwords'
3
+ require 'rubygems/package'
4
+ require 'fpm/fry/os_db'
5
+ require 'fpm/fry/source'
6
+ require 'fpm/fry/joined_io'
7
+ module FPM; module Fry
8
+ class DockerFile < Struct.new(:variables,:cache,:recipe)
9
+
10
+ class Source < Struct.new(:variables, :cache)
11
+
12
+ def initialize(variables, cache = Source::Null::Cache)
13
+ variables = variables.dup
14
+ if variables[:distribution] && !variables[:flavour] && OsDb[variables[:distribution]]
15
+ variables[:flavour] = OsDb[variables[:distribution]][:flavour]
16
+ end
17
+ variables.freeze
18
+ super(variables, cache)
19
+ end
20
+
21
+ def dockerfile
22
+ df = []
23
+ df << "FROM #{variables[:image]}"
24
+
25
+ df << "RUN mkdir /tmp/build"
26
+
27
+ cache.file_map.each do |from, to|
28
+ df << "ADD #{map_from(from)} #{map_to(to)}"
29
+ end
30
+
31
+ df << ""
32
+ return df.join("\n")
33
+ end
34
+
35
+ def tar_io
36
+ JoinedIO.new(
37
+ self_tar_io,
38
+ cache.tar_io
39
+ )
40
+ end
41
+
42
+ def self_tar_io
43
+ sio = StringIO.new
44
+ tar = Gem::Package::TarWriter.new(sio)
45
+ tar.add_file('Dockerfile','0777') do |io|
46
+ io.write(dockerfile)
47
+ end
48
+ #tar.close
49
+ sio.rewind
50
+ return sio
51
+ end
52
+
53
+ def map_to(dir)
54
+ if ['','.'].include? dir
55
+ return '/tmp/build'
56
+ else
57
+ return File.join('/tmp/build',dir)
58
+ end
59
+ end
60
+
61
+ def map_from(dir)
62
+ if dir == ''
63
+ return '.'
64
+ else
65
+ return dir
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ class Build < Struct.new(:base, :variables, :recipe)
72
+
73
+ attr :options
74
+ private :options
75
+
76
+ def initialize(base, variables, recipe, options = {})
77
+ variables = variables.dup
78
+ if variables[:distribution] && !variables[:flavour] && OsDb[variables[:distribution]]
79
+ variables[:flavour] = OsDb[variables[:distribution]][:flavour]
80
+ end
81
+ variables.freeze
82
+ @options = options.dup.freeze
83
+ super(base, variables, recipe)
84
+ end
85
+
86
+ def dockerfile
87
+ df = []
88
+ df << "FROM #{base}"
89
+ df << "WORKDIR /tmp/build"
90
+
91
+ deps = (recipe.build_depends.merge recipe.depends)\
92
+ .select{|_,v| v.fetch(:install,true) }\
93
+ .map do |k,v|
94
+ i = v.fetch(:install,true)
95
+ if i == true then
96
+ k
97
+ else
98
+ i
99
+ end
100
+ end.sort
101
+ if deps.any?
102
+ case(variables[:flavour])
103
+ when 'debian'
104
+ update = ''
105
+ if options[:update]
106
+ update = 'apt-get update && '
107
+ end
108
+ df << "RUN #{update}apt-get install --yes #{Shellwords.join(deps)}"
109
+ when 'redhat'
110
+ df << "RUN yum -y install #{Shellwords.join(deps)}"
111
+ else
112
+ raise "Unknown flavour: #{variables[:flavour]}"
113
+ end
114
+ end
115
+
116
+ df << "ADD .build.sh /tmp/build/"
117
+ df << "ENTRYPOINT /tmp/build/.build.sh"
118
+ df << ''
119
+ return df.join("\n")
120
+ end
121
+
122
+ def build_sh
123
+ df = ['#!/bin/bash']
124
+ df << 'set -e'
125
+ recipe.steps.each do |k,v|
126
+ df << "echo -e '\\e[1;32m====> #{Shellwords.escape k}\\e[0m'"
127
+ df << v.to_s
128
+ end
129
+ df << ''
130
+ return df.join("\n")
131
+ end
132
+
133
+ def tar_io
134
+ sio = StringIO.new
135
+ tar = Gem::Package::TarWriter.new(sio)
136
+ tar.add_file('.build.sh','0777') do |io|
137
+ io.write(build_sh)
138
+ end
139
+ tar.add_file('Dockerfile','0777') do |io|
140
+ io.write(dockerfile)
141
+ end
142
+ #tar.close
143
+ sio.rewind
144
+ return sio
145
+ end
146
+ end
147
+
148
+ end
149
+ end ; end
@@ -0,0 +1,63 @@
1
+ module FPM; module Fry
2
+ class JoinedIO
3
+ include Enumerable
4
+
5
+ def initialize(*ios)
6
+ @ios = ios
7
+ @pos = 0
8
+ @readbytes = 0
9
+ end
10
+
11
+ def read( len = nil )
12
+ buf = []
13
+ if len.nil?
14
+ while chunk = readpartial(512)
15
+ buf << chunk
16
+ @readbytes += chunk.bytesize
17
+ end
18
+ return buf.join
19
+ else
20
+ con = 0
21
+ while con < len
22
+ chunk = readpartial(len - con)
23
+ if chunk.nil?
24
+ if con == 0
25
+ return nil
26
+ else
27
+ return buf.join
28
+ end
29
+ end
30
+ @readbytes += chunk.bytesize
31
+ con += chunk.bytesize
32
+ buf << chunk
33
+ end
34
+ return buf.join
35
+ end
36
+ end
37
+
38
+ def readpartial( len )
39
+ while (io = @ios[@pos])
40
+ r = io.read( len )
41
+ if r.nil?
42
+ @pos = @pos + 1
43
+ next
44
+ else
45
+ return r
46
+ end
47
+ end
48
+ return nil
49
+ end
50
+
51
+ def pos
52
+ @readbytes
53
+ end
54
+
55
+ def eof?
56
+ @pos == @ios.size
57
+ end
58
+
59
+ def close
60
+ @ios.each(&:close)
61
+ end
62
+ end
63
+ end ; end
@@ -0,0 +1,35 @@
1
+ module FPM; module Fry
2
+
3
+ # Structure is
4
+ #
5
+ # <distribution> => {
6
+ # codenames: {
7
+ # <codename> => <version>
8
+ # },
9
+ # flavour: <flavour>
10
+ # }
11
+ OsDb = {
12
+ 'centos' => {
13
+ codenames: {},
14
+ flavour: 'redhat'
15
+ },
16
+
17
+ 'debian' => {
18
+ codenames: {
19
+ 'lenny' => '5',
20
+ 'squeeze' => '6',
21
+ 'wheezy' => '7'
22
+ },
23
+ flavour: 'debian'
24
+ },
25
+
26
+ 'ubuntu' => {
27
+ codenames: {
28
+ 'precise' => '12.04',
29
+ 'trusty' => '14.04'
30
+ },
31
+ flavour: 'debian'
32
+ }
33
+ }
34
+
35
+ end ; end
@@ -0,0 +1,90 @@
1
+ require 'fpm/fry/plugin'
2
+ module FPM::Fry::Plugin::Alternatives
3
+
4
+ BASH_HEADER = ['#!/bin/bash']
5
+ DEFAULT_PRIORITY = 10000
6
+ EXPECTED_KEYS = [:path, :link, :priority]
7
+
8
+ class DSL < Struct.new(:builder, :alternatives)
9
+
10
+ def initialize( b, a = {})
11
+ super
12
+ end
13
+
14
+ def []=(name, options={}, value)
15
+ name = name.to_s
16
+ if value.kind_of? String
17
+ options = normalize(name, options.merge(path: value) )
18
+ else
19
+ options = normalize(name, options.merge(value) )
20
+ end
21
+ alternatives[name] = options
22
+ end
23
+
24
+ def add(name, value, options={})
25
+ self[name, options] = value
26
+ end
27
+
28
+ def finish!
29
+ install = alternatives.map{|_,v| install_command(v) }
30
+ uninstall = alternatives.map{|_,v| uninstall_command(v) }
31
+ builder.plugin('script_helper') do
32
+ after_install_or_upgrade(*install)
33
+ before_remove_entirely(*uninstall)
34
+ end
35
+ end
36
+
37
+ private
38
+ def normalize_without_slaves(name, options)
39
+ if options.kind_of? String
40
+ options = {path: options}
41
+ elsif options.kind_of? Hash
42
+ additional_keys = options.keys - EXPECTED_KEYS
43
+ raise ArgumentError, "Unexpected options: #{additional_keys.inspect}" if additional_keys.any?
44
+ options = options.dup
45
+ else
46
+ raise ArgumentError, "Options must be either a Hash or a String, got #{options.inspect}"
47
+ end
48
+ options[:name] = name
49
+ options[:link] ||= File.join('/usr/bin',name)
50
+ return options
51
+ end
52
+
53
+ def normalize( name, options )
54
+ slaves = {}
55
+ if options.kind_of?(Hash) && options.key?(:slaves)
56
+ options = options.dup
57
+ slaves = options.delete(:slaves)
58
+ end
59
+ options = normalize_without_slaves(name, options)
60
+ options[:slaves] = slaves.map{|k,v| normalize_without_slaves(k, v) }
61
+ options[:priority] ||= DEFAULT_PRIORITY
62
+ return options
63
+ end
64
+
65
+ def install_command(options)
66
+ slaves = options.fetch(:slaves,[]).flat_map{|options| ['--slave', options[:link],options[:name],options[:path]] }
67
+ Shellwords.join(['update-alternatives','--install',options[:link],options[:name],options[:path],options[:priority].to_s, *slaves])
68
+ end
69
+
70
+ def uninstall_command(options)
71
+ Shellwords.join(['update-alternatives','--remove',options[:name],options[:path]])
72
+ end
73
+ end
74
+
75
+ def self.apply(builder, options = {}, &block)
76
+ dsl = DSL.new(builder)
77
+ options.each do |k,v|
78
+ dsl.add(k,v)
79
+ end
80
+ if block
81
+ if block.arity == 1
82
+ yield dsl
83
+ else
84
+ dsl.instance_eval(&block)
85
+ end
86
+ end
87
+ dsl.finish!
88
+ end
89
+
90
+ end
@@ -0,0 +1,66 @@
1
+ require 'fpm/fry/plugin'
2
+ require 'fileutils'
3
+ module FPM::Fry::Plugin::EditStaging
4
+
5
+ class AddFile < Struct.new(:path, :io, :options)
6
+ def call(_ , package)
7
+ file = package.staging_path(path)
8
+ FileUtils.mkdir_p(File.dirname(file))
9
+ File.open(file,'w') do | f |
10
+ IO.copy_stream(io, f)
11
+ if options[:chmod]
12
+ f.chmod(options[:chmod])
13
+ end
14
+ end
15
+ io.close if io.respond_to? :close
16
+ end
17
+ end
18
+
19
+ class LnS < Struct.new(:src, :dest)
20
+ def call(_ , package)
21
+ file = package.staging_path(dest)
22
+ FileUtils.mkdir_p(File.dirname(file))
23
+ File.symlink(src, file)
24
+ end
25
+ end
26
+
27
+ class DSL < Struct.new(:builder)
28
+ def add_file(path, io, options = {})
29
+ options = options.dup
30
+ options[:chmod] = convert_chmod(options[:chmod]) if options[:chmod]
31
+ options.freeze
32
+ io.rewind if io.respond_to? :rewind
33
+ builder.output_hooks << AddFile.new(path, io, options)
34
+ end
35
+
36
+ def ln_s(src, dest)
37
+ builder.output_hooks << LnS.new(src,dest)
38
+ end
39
+ private
40
+
41
+ def convert_chmod(chmod)
42
+ if chmod.kind_of? Numeric
43
+ num = chmod
44
+ elsif chmod.kind_of? String
45
+ num = chmod.to_i(8)
46
+ else
47
+ raise ArgumentError, "Invalid chmod format: #{chmod}"
48
+ end
49
+ return num
50
+ end
51
+
52
+ end
53
+
54
+ def self.apply(builder, &block)
55
+ d = DSL.new(builder)
56
+ if !block
57
+ return d
58
+ elsif block.arity == 1
59
+ block.call(d)
60
+ else
61
+ d.instance_eval(&block)
62
+ end
63
+ return d
64
+ end
65
+
66
+ end
@@ -0,0 +1,18 @@
1
+ require 'fpm/fry/plugin'
2
+ module FPM::Fry::Plugin::Exclude
3
+
4
+ class Exclude < Struct.new(:matches)
5
+
6
+ def call(_, package)
7
+ (package.attributes[:excludes] ||= []).push(*matches)
8
+ end
9
+
10
+ end
11
+
12
+ def exclude(*matches)
13
+ return if matches.none?
14
+ input_hooks << Exclude.new(matches)
15
+ end
16
+
17
+ end
18
+
@@ -0,0 +1,53 @@
1
+ require 'fpm/fry/plugin'
2
+ module FPM::Fry::Plugin::Init
3
+
4
+ def self.detect_init(variables)
5
+ if variables[:init]
6
+ return variables[:init]
7
+ end
8
+ d = variables[:distribution]
9
+ v = variables[:distribution_version].split('.').map(&:to_i)
10
+ case(d)
11
+ when 'debian'
12
+ if v[0] < 8
13
+ return 'sysv'
14
+ else
15
+ return 'systemd'
16
+ end
17
+ when 'ubuntu'
18
+ if v[0] <= 14 && v[1] < 10
19
+ return 'upstart'
20
+ else
21
+ return 'systemd'
22
+ end
23
+ when 'centos','redhat'
24
+ if v[0] <= 5
25
+ return 'sysv'
26
+ elsif v[0] == 6
27
+ return 'upstart'
28
+ else
29
+ return 'systemd'
30
+ end
31
+ else
32
+ raise "Unknown init system for #{d} #{v.join '.'}"
33
+ end
34
+ end
35
+
36
+ def init(*inits)
37
+ inits = inits.flatten.map(&:to_s)
38
+ actual = FPM::Fry::Plugin::Init.detect_init(variables)
39
+ if inits.none?
40
+ return actual
41
+ elsif inits.include? actual
42
+ if block_given?
43
+ yield
44
+ else
45
+ return true
46
+ end
47
+ else
48
+ return false
49
+ end
50
+ end
51
+
52
+
53
+ end
@@ -0,0 +1,10 @@
1
+ module FPM::Fry::Plugin::Platforms
2
+
3
+ def platforms(platform, *platforms)
4
+ p = [platform,platforms].flatten.map(&:to_s)
5
+ if p.include? distribution
6
+ yield
7
+ end
8
+ end
9
+
10
+ end