fpm-fry 0.1.3

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