picsolve_docker_builder 0.1.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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +8 -0
  5. data/.travis.yml +3 -0
  6. data/Gemfile +4 -0
  7. data/README.md +25 -0
  8. data/Rakefile +80 -0
  9. data/bin/docker_build +5 -0
  10. data/integration/integration_play_spec.rb +51 -0
  11. data/integration/play_hello_world/.docker-builder.yml +3 -0
  12. data/integration/play_hello_world/.gitignore +12 -0
  13. data/integration/play_hello_world/Gemfile +2 -0
  14. data/integration/play_hello_world/LICENSE +8 -0
  15. data/integration/play_hello_world/README +4 -0
  16. data/integration/play_hello_world/Rakefile +3 -0
  17. data/integration/play_hello_world/app/controllers/Application.scala +12 -0
  18. data/integration/play_hello_world/app/views/index.scala.html +7 -0
  19. data/integration/play_hello_world/app/views/main.scala.html +15 -0
  20. data/integration/play_hello_world/build.sbt +20 -0
  21. data/integration/play_hello_world/conf/application.conf +44 -0
  22. data/integration/play_hello_world/conf/logback.xml +22 -0
  23. data/integration/play_hello_world/conf/routes +9 -0
  24. data/integration/play_hello_world/project/build.properties +4 -0
  25. data/integration/play_hello_world/project/plugins.sbt +16 -0
  26. data/integration/play_hello_world/public/images/favicon.png +0 -0
  27. data/integration/play_hello_world/public/javascripts/hello.js +3 -0
  28. data/integration/play_hello_world/public/stylesheets/main.css +0 -0
  29. data/integration/play_hello_world/test/ApplicationSpec.scala +30 -0
  30. data/integration/play_hello_world/test/IntegrationSpec.scala +24 -0
  31. data/integration/spec_helper.rb +3 -0
  32. data/lib/picsolve_docker_builder.rb +5 -0
  33. data/lib/picsolve_docker_builder/base.rb +66 -0
  34. data/lib/picsolve_docker_builder/builder/builder.rb +113 -0
  35. data/lib/picsolve_docker_builder/builder/file.rb +46 -0
  36. data/lib/picsolve_docker_builder/composer/composer.rb +134 -0
  37. data/lib/picsolve_docker_builder/composer/image.rb +68 -0
  38. data/lib/picsolve_docker_builder/composer/registry.rb +80 -0
  39. data/lib/picsolve_docker_builder/frame.rb +298 -0
  40. data/lib/picsolve_docker_builder/helpers/kubeclient.rb +34 -0
  41. data/lib/picsolve_docker_builder/helpers/kubernetes/pod.rb +38 -0
  42. data/lib/picsolve_docker_builder/helpers/kubernetes/rc.rb +98 -0
  43. data/lib/picsolve_docker_builder/helpers/kubernetes/resource.rb +28 -0
  44. data/lib/picsolve_docker_builder/helpers/kubernetes/service.rb +50 -0
  45. data/lib/picsolve_docker_builder/helpers/kubernetes_manager.rb +102 -0
  46. data/lib/picsolve_docker_builder/helpers/repository.rb +24 -0
  47. data/lib/picsolve_docker_builder/helpers/ssh_forward.rb +75 -0
  48. data/lib/picsolve_docker_builder/scala.rb +196 -0
  49. data/lib/picsolve_docker_builder/version.rb +4 -0
  50. data/lib/tasks/compose.rake +25 -0
  51. data/lib/tasks/docker.rake +24 -0
  52. data/lib/tasks/scala.rake +11 -0
  53. data/picsolve_docker_builder.gemspec +35 -0
  54. metadata +250 -0
@@ -0,0 +1,24 @@
1
+ import org.specs2.mutable._
2
+ import org.specs2.runner._
3
+ import org.junit.runner._
4
+
5
+ import play.api.test._
6
+ import play.api.test.Helpers._
7
+
8
+ /**
9
+ * add your integration spec here.
10
+ * An integration test will fire up a whole play application in a real (or headless) browser
11
+ */
12
+ @RunWith(classOf[JUnitRunner])
13
+ class IntegrationSpec extends Specification {
14
+
15
+ "Application" should {
16
+
17
+ "work from within a browser" in new WithBrowser {
18
+
19
+ browser.goTo("http://localhost:" + port)
20
+
21
+ browser.pageSource must contain("Your new application is ready.")
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,3 @@
1
+ RSpec.configure do |config|
2
+ config.mock_with :rspec
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'picsolve_docker_builder/version'
2
+
3
+ # Tool that support the build and deployment of docker images/containers
4
+ module PicsolveDockerBuilder
5
+ end
@@ -0,0 +1,66 @@
1
+ require 'logger'
2
+ require 'psych'
3
+
4
+ # add a deep_merge function to hashes
5
+ # rubocop:disable Style/CaseEquality, Style/ClassAndModuleChildren
6
+ class ::Hash
7
+ def deep_merge(second)
8
+ merger = proc do |_key, v1, v2|
9
+ if Hash === v1 && Hash === v2
10
+ v1.merge(v2, &merger)
11
+ else
12
+ v2
13
+ end
14
+ end
15
+ merge(second, &merger)
16
+ end
17
+ end
18
+
19
+ module PicsolveDockerBuilder
20
+ # Base class for everything
21
+ # * logging
22
+ # * accessing config
23
+ module Base
24
+ def create_logger
25
+ log = Logger.new(STDOUT)
26
+ log.level = Logger::DEBUG
27
+ log
28
+ end
29
+
30
+ def log
31
+ @logger ||= create_logger
32
+ end
33
+
34
+ def config
35
+ @config ||= read_config
36
+ end
37
+
38
+ def base_dir
39
+ Dir.pwd
40
+ end
41
+
42
+ def config_path
43
+ File.join(base_dir, '.docker-builder.yml')
44
+ end
45
+
46
+ def default_config
47
+ {}
48
+ end
49
+
50
+ def validate_config(c)
51
+ c
52
+ end
53
+
54
+ def read_config
55
+ c = default_config
56
+ path = config_path
57
+ begin
58
+ yaml = Psych.load_file path
59
+ c = c.deep_merge(yaml)
60
+ rescue Errno::ENOENT
61
+ log.debug "can not find config at '#{path}'"
62
+ end
63
+ validate_config(c)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,113 @@
1
+ require 'rubygems/package'
2
+ require 'zlib'
3
+ require 'picsolve_docker_builder/base'
4
+ require 'picsolve_docker_builder/builder/file'
5
+
6
+ module PicsolveDockerBuilder
7
+ module Builder
8
+ # This Builder supports the building process of an docker image
9
+ class Builder
10
+ include PicsolveDockerBuilder::Base
11
+ def initialize(opts = {})
12
+ @opts = opts
13
+ end
14
+
15
+ def maintainer
16
+ @opts[:maintainer]
17
+ end
18
+
19
+ def base_image
20
+ @opts[:base_image]
21
+ end
22
+
23
+ def ports
24
+ ports = @opts[:ports]
25
+ ports = [ports] unless ports.is_a?(Array)
26
+ ports.map do |port|
27
+ "EXPOSE #{port}"
28
+ end.join('\n')
29
+ end
30
+
31
+ def adds
32
+ files = @opts[:files].select(&:add_to_image?)
33
+ files.map! do |f|
34
+ "ADD #{f.path} #{f.destination}"
35
+ end
36
+ files.join("\n")
37
+ end
38
+
39
+ def command
40
+ "[\"#{@opts[:command].join('", "')}\"]"
41
+ end
42
+
43
+ def hook(name)
44
+ v = @opts[:hooks][name]
45
+ v = [v] unless v.is_a?(Array)
46
+ v.join("\n") + "\n"
47
+ end
48
+
49
+ def required_attrs
50
+ [
51
+ :base_image,
52
+ :maintainer,
53
+ :ports,
54
+ :command,
55
+ :files
56
+ ]
57
+ end
58
+
59
+ def build_tar
60
+ tarfile = StringIO.new('')
61
+ Gem::Package::TarWriter.new(tarfile) do |tar|
62
+ # add Dockerfile to tar
63
+ dockerfile.each_line do |line|
64
+ log.debug "docker_build Dockerfile: #{line.strip}"
65
+ end
66
+ tar.add_file 'Dockerfile', 0644 do |tf|
67
+ tf.write dockerfile
68
+ end
69
+
70
+ # add other files to tar
71
+ @opts[:files].each do |f|
72
+ f.add_to_tar tar
73
+ end
74
+ end
75
+ StringIO.new(tarfile.string)
76
+ end
77
+
78
+ # Overwrite docker_build and filter build assets
79
+ def build
80
+ log.info 'docker build starting...'
81
+ Docker::Image.build_from_tar(build_tar) do |stream|
82
+ s = JSON.parse(stream)['stream']
83
+ log.debug s.strip unless s.nil?
84
+ end
85
+ rescue NoMethodError => e
86
+ log.fatal "docker build failed: #{e}"
87
+ exit 1
88
+ end
89
+
90
+ def dockerfile
91
+ <<EOS
92
+ FROM #{base_image}
93
+ MAINTAINER #{maintainer}
94
+
95
+ #{hook(:early)}
96
+
97
+ #{hook(:before_adds)}
98
+
99
+ #{adds}
100
+
101
+ #{hook(:after_adds)}
102
+
103
+ #{ports}
104
+
105
+ # call run script
106
+ CMD #{command}
107
+
108
+ #{hook(:late)}
109
+ EOS
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,46 @@
1
+ require 'picsolve_docker_builder/base'
2
+
3
+ module PicsolveDockerBuilder
4
+ module Builder
5
+ # File helper to add files to tars
6
+ class File
7
+ include PicsolveDockerBuilder::Base
8
+ attr_reader :path
9
+ def initialize(path, opts = {})
10
+ @opts = opts
11
+ @path = path
12
+ end
13
+
14
+ def content
15
+ return @opts[:content] if @opts.key? :content
16
+
17
+ return read_source if @opts.key? :source
18
+
19
+ fail 'No content found'
20
+ end
21
+
22
+ def destination
23
+ @opts[:destination]
24
+ end
25
+
26
+ def add_to_image?
27
+ @opts.key? :destination
28
+ end
29
+
30
+ def source
31
+ @opts[:source]
32
+ end
33
+
34
+ def mode
35
+ ::File.stat(source).mode
36
+ end
37
+
38
+ def add_to_tar(tar)
39
+ log.debug "add file from #{source} to #{path} in tar"
40
+ tar.add_file path, mode do |tf|
41
+ ::File.open(source, 'rb') { |f| tf.write f.read }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,134 @@
1
+ require 'picsolve_docker_builder/base'
2
+ require 'picsolve_docker_builder/helpers/kubernetes_manager'
3
+ require 'picsolve_docker_builder/composer/image'
4
+ require 'excon'
5
+ require 'psych'
6
+ require 'securerandom'
7
+
8
+ module PicsolveDockerBuilder
9
+ module Composer
10
+ # The composer parses a docker-compose.yml and tries to spin up the
11
+ # containers in a kubernetes cluster
12
+ class Composer
13
+ include PicsolveDockerBuilder::Base
14
+ attr_reader :stage
15
+
16
+ def initialize
17
+ end
18
+
19
+ def prepare
20
+ images
21
+ end
22
+
23
+ def stage=(my_stage)
24
+ @stage = my_stage.downcase
25
+ end
26
+
27
+ def deploy
28
+ log.info "start deploying app_name=#{app_name} to stage #{stage}"
29
+
30
+ # reset hash
31
+ @hash = nil
32
+
33
+ # deploy services
34
+ images.each do |i|
35
+ i.service.deploy
36
+ end
37
+
38
+ # deploy rcs
39
+ images.each do |i|
40
+ i.rc.deploy
41
+ end
42
+
43
+ # wait for deployed rc
44
+ wait_timeout = 300
45
+ wait_sleep = 2
46
+ wait_beginning = 15
47
+ wait_count = wait_beginning
48
+ unready_images = images.dup
49
+ log.info \
50
+ 'Wait for the new pods to be up and running for at least 20 seconds'
51
+ sleep wait_beginning
52
+ loop do
53
+ unready_images.each do |i|
54
+ unready_images.delete(i) if i.rc.ready?
55
+ end
56
+ log.debug "Still waiting for: #{unready_images.map(&:name)}"
57
+ wait_count += wait_sleep
58
+ break if wait_count > wait_timeout
59
+ break if unready_images.length == 0
60
+ sleep wait_sleep
61
+ end
62
+
63
+ # build list of successful deplyoed images
64
+ successful_images = images.dup
65
+ unready_images.each do |u|
66
+ successful_images.delete(u)
67
+ end
68
+
69
+ # remove old rcs of successful deployed images
70
+ successful_images.each do |i|
71
+ log.debug "Remove pods for #{i.name}}"
72
+ i.rc.remove_old_rcs
73
+ end
74
+
75
+ fail "Failed to deploy this service: #{unready_images.map(&:name)}" \
76
+ if unready_images.length > 0
77
+ end
78
+
79
+ def app_name
80
+ config['compose']['app_name'].downcase.gsub(/[^a-z0-9\-]/, '-')
81
+ end
82
+
83
+ def namespace
84
+ "#{app_name}-#{stage}".downcase
85
+ end
86
+
87
+ def kubernetes
88
+ @kubernetes ||= PicsolveDockerBuilder::Helpers::KubernetesManager.new
89
+ end
90
+
91
+ def hash
92
+ @hash ||= SecureRandom.hex[0..7]
93
+ end
94
+
95
+ def default_config
96
+ { 'compose' => {} }
97
+ end
98
+
99
+ def images
100
+ @images ||= read_images
101
+ end
102
+
103
+ def read_images
104
+ i = []
105
+ docker_compose.each do |name, config|
106
+ i << Image.new(name, config, self)
107
+ end
108
+ i
109
+ end
110
+
111
+ def docker_compose
112
+ @docker_compose ||= read_docker_compose
113
+ end
114
+
115
+ def base_dir
116
+ Dir.pwd
117
+ end
118
+
119
+ def docker_compose_path
120
+ File.join(base_dir, 'docker-compose.yml')
121
+ end
122
+
123
+ def read_docker_compose
124
+ path = docker_compose_path
125
+ Psych.load_file path
126
+ rescue Errno::ENOENT
127
+ log.debug "can not find docker-compose.yml at '#{path}'"
128
+ end
129
+
130
+ def query_registry
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,68 @@
1
+ require 'picsolve_docker_builder/base'
2
+ require 'picsolve_docker_builder/composer/registry'
3
+
4
+ module PicsolveDockerBuilder
5
+ module Composer
6
+ # Represents an image in the registry
7
+ class Image
8
+ include PicsolveDockerBuilder::Base
9
+ attr_reader :config, :name, :composer
10
+ def initialize(name, config, composer)
11
+ log.info "Image name=#{name} config=#{config}"
12
+ @config = config
13
+ @name = name
14
+ @composer = composer
15
+ log.info "repo_tag_uniqe is #{repo_tag_unique}"
16
+ end
17
+
18
+ def repo_tag
19
+ config['image']
20
+ end
21
+
22
+ def kubernetes
23
+ composer.kubernetes
24
+ end
25
+
26
+ def rc
27
+ @rc ||= kubernetes.rc self
28
+ end
29
+
30
+ def service
31
+ @service ||= kubernetes.service self
32
+ end
33
+
34
+ def ports
35
+ config['ports'].map do |port|
36
+ port_split = port.split(/:/)
37
+ {
38
+ 'port' => port_split[1].to_i
39
+ }
40
+ end
41
+ rescue StandardError
42
+ []
43
+ end
44
+
45
+ def ports_rc
46
+ p = []
47
+ ports.each do |port|
48
+ p << {
49
+ 'containerPort' => port['port']
50
+ }
51
+ end
52
+ p
53
+ end
54
+
55
+ def repo_tag_hash_unique
56
+ @repo_tag_hash_unique ||= Registry.repo_tag_unique(config['image'])
57
+ end
58
+
59
+ def repo_tag_unique
60
+ repo_tag_hash_unique[:tag_unique]
61
+ end
62
+
63
+ def hash
64
+ repo_tag_hash_unique[:hash]
65
+ end
66
+ end
67
+ end
68
+ end