dry-dock 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.
- checksums.yaml +7 -0
- data/.dockerignore +6 -0
- data/.pryrc +1 -0
- data/.rspec +2 -0
- data/Dockerfile +69 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +102 -0
- data/LICENSE +22 -0
- data/README.md +75 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/bin/drydock +45 -0
- data/bin/json-test-consumer.rb +11 -0
- data/bin/json-test-producer.rb +25 -0
- data/bin/test-tar-writer-digest.rb +27 -0
- data/dry-dock.gemspec +135 -0
- data/examples/ruby-dsl.rb +14 -0
- data/examples/ruby-node-app-dsl.rb +128 -0
- data/examples/test-dsl.rb +9 -0
- data/examples/test.rb +46 -0
- data/lib/drydock/cli_flags.rb +46 -0
- data/lib/drydock/container_config.rb +75 -0
- data/lib/drydock/docker_api_patch.rb +176 -0
- data/lib/drydock/drydock.rb +65 -0
- data/lib/drydock/errors.rb +6 -0
- data/lib/drydock/file_manager.rb +26 -0
- data/lib/drydock/formatters.rb +13 -0
- data/lib/drydock/ignorefile_definition.rb +61 -0
- data/lib/drydock/image_repository.rb +50 -0
- data/lib/drydock/logger.rb +61 -0
- data/lib/drydock/object_caches/base.rb +24 -0
- data/lib/drydock/object_caches/filesystem_cache.rb +88 -0
- data/lib/drydock/object_caches/in_memory_cache.rb +52 -0
- data/lib/drydock/object_caches/no_cache.rb +38 -0
- data/lib/drydock/phase.rb +50 -0
- data/lib/drydock/phase_chain.rb +233 -0
- data/lib/drydock/plugins/apk.rb +31 -0
- data/lib/drydock/plugins/base.rb +15 -0
- data/lib/drydock/plugins/npm.rb +16 -0
- data/lib/drydock/plugins/package_manager.rb +30 -0
- data/lib/drydock/plugins/rubygems.rb +30 -0
- data/lib/drydock/project.rb +427 -0
- data/lib/drydock/runtime_options.rb +79 -0
- data/lib/drydock/stream_monitor.rb +54 -0
- data/lib/drydock/tar_writer.rb +36 -0
- data/lib/drydock.rb +35 -0
- data/spec/assets/MANIFEST +4 -0
- data/spec/assets/hello-world.txt +1 -0
- data/spec/assets/sample.tar +0 -0
- data/spec/assets/test.sh +3 -0
- data/spec/drydock/cli_flags_spec.rb +38 -0
- data/spec/drydock/container_config_spec.rb +230 -0
- data/spec/drydock/docker_api_patch_spec.rb +103 -0
- data/spec/drydock/drydock_spec.rb +25 -0
- data/spec/drydock/file_manager_spec.rb +53 -0
- data/spec/drydock/formatters_spec.rb +26 -0
- data/spec/drydock/ignorefile_definition_spec.rb +123 -0
- data/spec/drydock/image_repository_spec.rb +54 -0
- data/spec/drydock/object_caches/base_spec.rb +28 -0
- data/spec/drydock/object_caches/filesystem_cache_spec.rb +48 -0
- data/spec/drydock/object_caches/no_cache_spec.rb +62 -0
- data/spec/drydock/phase_chain_spec.rb +118 -0
- data/spec/drydock/phase_spec.rb +67 -0
- data/spec/drydock/plugins/apk_spec.rb +49 -0
- data/spec/drydock/plugins/base_spec.rb +13 -0
- data/spec/drydock/plugins/npm_spec.rb +26 -0
- data/spec/drydock/plugins/package_manager_spec.rb +12 -0
- data/spec/drydock/plugins/rubygems_spec.rb +53 -0
- data/spec/drydock/project_import_spec.rb +39 -0
- data/spec/drydock/project_spec.rb +156 -0
- data/spec/drydock/runtime_options_spec.rb +31 -0
- data/spec/drydock/stream_monitor_spec.rb +41 -0
- data/spec/drydock/tar_writer_spec.rb +27 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/support/shared_examples/base_class.rb +3 -0
- data/spec/support/shared_examples/container_config.rb +12 -0
- data/spec/support/shared_examples/drydockfile.rb +6 -0
- metadata +223 -0
| @@ -0,0 +1,128 @@ | |
| 1 | 
            +
            #!/usr/bin/env drydock
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            APP_ROOT   = '/app'
         | 
| 4 | 
            +
            BUILD_ROOT = '/build'
         | 
| 5 | 
            +
            REPO_NAME  = 'ripta/test'
         | 
| 6 | 
            +
            TAG_NAME   = 'v1.0'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
             | 
| 9 | 
            +
            from 'gliderlabs/alpine', '3.2'
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            with Plugins::APK do |pkg|
         | 
| 12 | 
            +
              pkg.update
         | 
| 13 | 
            +
              pkg.upgrade
         | 
| 14 | 
            +
              pkg.add 'ruby', 'ruby-dev'
         | 
| 15 | 
            +
              pkg.add 'nodejs', 'nodejs-dev'
         | 
| 16 | 
            +
              pkg.add 'musl', 'musl-dev'
         | 
| 17 | 
            +
              pkg.add 'linux-headers'
         | 
| 18 | 
            +
              pkg.add 'gcc', 'g++'
         | 
| 19 | 
            +
              pkg.add 'make'
         | 
| 20 | 
            +
              pkg.add 'curl', 'curl-dev'
         | 
| 21 | 
            +
              pkg.add 'openssh'
         | 
| 22 | 
            +
              pkg.add 'libffi-dev', 'libxml2-dev', 'libxslt-dev'
         | 
| 23 | 
            +
              pkg.add 'git'
         | 
| 24 | 
            +
            end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            download_once 'https://github.com/tianon/gosu/releases/download/1.3/gosu-amd64', '/bin/gosu', chmod: 0755
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            with Plugins::Rubygems do |g|
         | 
| 29 | 
            +
              # g.source.add 'https://s3.amazonaws.com/production.s3.rubygems.org/'
         | 
| 30 | 
            +
              # g.source.remove 'https://rubygems.org/'
         | 
| 31 | 
            +
              g.update_system(document: false)
         | 
| 32 | 
            +
              g.install('bundler', document: false)
         | 
| 33 | 
            +
              g.install('unicorn', document: false)
         | 
| 34 | 
            +
            end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            run 'bundle config --global frozen 1'
         | 
| 37 | 
            +
            run 'bundle config --global build.nokogiri --use-system-libraries'
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            with Plugins::NPM do |npm|
         | 
| 40 | 
            +
              npm.install('bower', 'gulp', global: true)
         | 
| 41 | 
            +
            end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            one_week_old = CachingStrategies::TimeLimited.new(
         | 
| 44 | 
            +
              max_age:   1.week,
         | 
| 45 | 
            +
              max_depth: 1
         | 
| 46 | 
            +
            )
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            derive do
         | 
| 49 | 
            +
              env 'BUILD_ROOT', BUILD_ROOT
         | 
| 50 | 
            +
              mkdir BUILD_ROOT
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              env 'APP_ROOT', APP_ROOT
         | 
| 53 | 
            +
              mkdir APP_ROOT
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              gem_image = derive(label: 'rubygems', strategy: one_week_old) do
         | 
| 56 | 
            +
                copy 'Gemfile', BUILD_ROOT
         | 
| 57 | 
            +
                copy 'Gemfile.lock', BUILD_ROOT
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                cd BUILD_ROOT do
         | 
| 60 | 
            +
                  run 'bundle --path vendor'
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              npm_image = if File.exist?('package.json')
         | 
| 65 | 
            +
                derive(label: 'npm', strategy: one_week_old) do
         | 
| 66 | 
            +
                  copy 'package.json', BUILD_ROOT
         | 
| 67 | 
            +
                  cd BUILD_ROOT do
         | 
| 68 | 
            +
                    with(Plugins::NPM).install
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
              import 'vendor',       from: BUILD_ROOT, in: gem_image, to: APP_ROOT
         | 
| 74 | 
            +
              import 'node_modules', from: BUILD_ROOT, in: npm_image, to: APP_ROOT
         | 
| 75 | 
            +
              # import_stream gem_image.export_stream(BUILD_ROOT / 'vendor'), APP_ROOT / 'vendor'
         | 
| 76 | 
            +
              # import_stream npm_image.export_stream(BUILD_ROOT / 'node_modules'), APP_ROOT / 'node_modules'
         | 
| 77 | 
            +
              copy '.', BUILD_ROOT
         | 
| 78 | 
            +
             | 
| 79 | 
            +
              # import_stream BUILD_ROOT do |writer|
         | 
| 80 | 
            +
              #   gem_image.export_stream(BUILD_ROOT + '/vendor') do |reader|
         | 
| 81 | 
            +
              #     while chunk = reader.read(32 * 1024)
         | 
| 82 | 
            +
              #       writer.write(chunk)
         | 
| 83 | 
            +
              #     end
         | 
| 84 | 
            +
              #   end
         | 
| 85 | 
            +
              # end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
              # tag REPO_NAME, TAG_NAME
         | 
| 88 | 
            +
            end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
            # # Drydock.using(dd) { |base| ... }
         | 
| 91 | 
            +
            # #   or
         | 
| 92 | 
            +
            # # base = Drydock.from(dd.id)
         | 
| 93 | 
            +
            # #   ...
         | 
| 94 | 
            +
            # Drydock.using(dd) do |base|
         | 
| 95 | 
            +
            #   base.env('BUILD_ROOT', build_root)
         | 
| 96 | 
            +
            #   base.mkdir(build_root)
         | 
| 97 | 
            +
             | 
| 98 | 
            +
            #   # dd.snapshot('name') do |base|
         | 
| 99 | 
            +
            #   #   ...
         | 
| 100 | 
            +
            #   # end
         | 
| 101 | 
            +
            #   #   or
         | 
| 102 | 
            +
            #   # base = Drydock.from('name') || Drydock.from(dd.id)
         | 
| 103 | 
            +
            #   #   ...
         | 
| 104 | 
            +
            #   # base.tag('name')
         | 
| 105 | 
            +
            #   bundle_image = base.snapshot('rubygems', max_age: Date.beginning_of_week) do |build|
         | 
| 106 | 
            +
            #     build.copy('Gemfile', build_root)
         | 
| 107 | 
            +
            #     build.copy('Gemfile.lock', build_root)
         | 
| 108 | 
            +
            #     build.in(build_root).run('bundle --path vendor')
         | 
| 109 | 
            +
            #   end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
            #   npm_image = base.snapshot('npm', max_age: Date.beginning_of_week) do |build|
         | 
| 112 | 
            +
            #     build.copy('package.json', build_root)
         | 
| 113 | 
            +
            #     build.in(build_root).npm.install
         | 
| 114 | 
            +
            #   end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
            #   Drydock.using(dd) do |build|
         | 
| 117 | 
            +
            #     build.env('APPLICATION_ROOT', app_root)
         | 
| 118 | 
            +
            #     build.mkdir(app_root)
         | 
| 119 | 
            +
              
         | 
| 120 | 
            +
            #     build.copy('.', app_root)
         | 
| 121 | 
            +
            #     build.import(bundle_image.export(build_root), app_root)
         | 
| 122 | 
            +
            #     build.import(npm_image.export(build_root), app_root)
         | 
| 123 | 
            +
             | 
| 124 | 
            +
            #     build.tag('ripta/dumplings', 'v1.0')
         | 
| 125 | 
            +
            #   end
         | 
| 126 | 
            +
            # end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
             | 
    
        data/examples/test.rb
    ADDED
    
    | @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'rubygems'
         | 
| 4 | 
            +
            require 'bundler'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Bundler.setup
         | 
| 7 | 
            +
            require 'docker'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Docker.options[:read_timeout] = 300
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            estream = Thread.new do
         | 
| 12 | 
            +
              Docker::Event.stream do |event|
         | 
| 13 | 
            +
                puts event
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
            end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            stream_chunk_proc = proc do |stream, chunk|
         | 
| 18 | 
            +
              puts "#{stream} #{chunk}"
         | 
| 19 | 
            +
            end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            i1 = Docker::Image.create(fromImage: 'gliderlabs/alpine', tag: '3.2')
         | 
| 22 | 
            +
            puts i1.id
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            c1 = Docker::Container.create(Image: i1.id, Cmd: ['/bin/sh', '-c', 'apk update'], Tty: true)
         | 
| 25 | 
            +
            c1.tap(&:start).attach(&stream_chunk_proc)
         | 
| 26 | 
            +
            c1.wait
         | 
| 27 | 
            +
            puts "  #{c1.changes.size} changed files"
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            i2 = c1.commit
         | 
| 30 | 
            +
            puts i2.id
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            Thread.kill(estream)
         | 
| 33 | 
            +
            exit
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            c2 = Docker::Container.create(Image: i2.id, Cmd: ['/bin/sh', '-c', 'apk add ruby ruby-dev'], Tty: true)
         | 
| 36 | 
            +
            c2.tap(&:start).attach(&stream_chunk_proc)
         | 
| 37 | 
            +
            c2.wait
         | 
| 38 | 
            +
            puts "  #{c2.changes.size} changed files"
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            i3 = c2.commit
         | 
| 41 | 
            +
            puts i3.id
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            ##image.run('apk update')
         | 
| 44 | 
            +
            ##image.run('apk add ruby ruby-dev')
         | 
| 45 | 
            +
            ##image.run('apk add nodejs nodejs-dev')
         | 
| 46 | 
            +
             | 
| @@ -0,0 +1,46 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Drydock
         | 
| 3 | 
            +
              class CliFlags
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(flags = {})
         | 
| 6 | 
            +
                  @flags = flags
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def to_s
         | 
| 10 | 
            +
                  return '' if flags.nil? || flags.empty?
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  buffer = StringIO.new
         | 
| 13 | 
            +
                  flags.each_pair do |k, v|
         | 
| 14 | 
            +
                    buffer << process_flag(k, v)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  buffer.string
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                private
         | 
| 21 | 
            +
                attr_reader :flags
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def process_flag(key, value)
         | 
| 24 | 
            +
                  key = key.to_s
         | 
| 25 | 
            +
                  if key.size == 1
         | 
| 26 | 
            +
                    "-#{key} "
         | 
| 27 | 
            +
                  else
         | 
| 28 | 
            +
                    key = key.gsub(/_/, '-')
         | 
| 29 | 
            +
                    case value
         | 
| 30 | 
            +
                    when TrueClass
         | 
| 31 | 
            +
                      "--#{key} "
         | 
| 32 | 
            +
                    when FalseClass
         | 
| 33 | 
            +
                      "--no-#{key} "
         | 
| 34 | 
            +
                    else
         | 
| 35 | 
            +
                      "--#{key} #{process_value(value)}"
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def process_value(value)
         | 
| 41 | 
            +
                  value = value.to_s
         | 
| 42 | 
            +
                  value.match(/\s/) ? value.inspect : value
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
            end
         | 
| @@ -0,0 +1,75 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Drydock
         | 
| 3 | 
            +
              class ContainerConfig < ::Hash
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                DEFAULTS = {
         | 
| 6 | 
            +
                  'OpenStdin'    => false,
         | 
| 7 | 
            +
                  'AttachStdin'  => false,
         | 
| 8 | 
            +
                  'AttachStdout' => false,
         | 
| 9 | 
            +
                  'AttachStderr' => false,
         | 
| 10 | 
            +
                  'User'         => '',
         | 
| 11 | 
            +
                  'Tty'          => false,
         | 
| 12 | 
            +
                  'Cmd'          => nil,
         | 
| 13 | 
            +
                  'Env'          => ['PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'],
         | 
| 14 | 
            +
                  'Labels'       => nil,
         | 
| 15 | 
            +
                  'Entrypoint'   => nil,
         | 
| 16 | 
            +
                  'ExposedPorts' => nil,
         | 
| 17 | 
            +
                  'Volumes'      => nil
         | 
| 18 | 
            +
                }
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def self.from(hash)
         | 
| 21 | 
            +
                  return nil if hash.nil?
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  self.new.tap do |cfg|
         | 
| 24 | 
            +
                    DEFAULTS.each_pair do |k, v|
         | 
| 25 | 
            +
                      cfg[k] = v
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                    hash.each_pair do |k, v|
         | 
| 28 | 
            +
                      cfg[k] = v
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                # Logic taken from https://github.com/docker/docker/blob/master/runconfig/compare.go
         | 
| 34 | 
            +
                def ==(other)
         | 
| 35 | 
            +
                  return false if other.nil?
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  return false if self['OpenStdin'] || other['OpenStdin']
         | 
| 38 | 
            +
                  return false if self['AttachStdout'] != other['AttachStdout']
         | 
| 39 | 
            +
                  return false if self['AttachStderr'] != other['AttachStderr']
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  return false if self['User'] != other['User']
         | 
| 42 | 
            +
                  return false if self['Tty'] != other['Tty']
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  return false if self['Cmd'] != other['Cmd']
         | 
| 45 | 
            +
                  return false if Array(self['Env']).sort != Array(other['Env']).sort
         | 
| 46 | 
            +
                  return false if self['Labels'] != other['Labels']
         | 
| 47 | 
            +
                  return false if self['Entrypoint'] != other['Entrypoint']
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  my_ports = self['ExposedPorts'] || {}
         | 
| 50 | 
            +
                  other_ports = other['ExposedPorts'] || {}
         | 
| 51 | 
            +
                  return false if my_ports.keys.size != other_ports.keys.size
         | 
| 52 | 
            +
                  my_ports.keys.each do |my_port|
         | 
| 53 | 
            +
                    return false unless other_ports.key?(my_port)
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  my_vols = self['Volumes'] || {}
         | 
| 57 | 
            +
                  other_vols = other['Volumes'] || {}
         | 
| 58 | 
            +
                  return false if my_vols.keys.size != other_vols.keys.size
         | 
| 59 | 
            +
                  my_vols.keys.each do |my_vol|
         | 
| 60 | 
            +
                    return false unless other_vols.key?(my_vol)
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  return true
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def [](key)
         | 
| 67 | 
            +
                  super(key.to_s)
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def []=(key, value)
         | 
| 71 | 
            +
                  super(key.to_s, value)
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
            end
         | 
| @@ -0,0 +1,176 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Docker
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              class Connection
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def raw_request(*args, &block)
         | 
| 7 | 
            +
                  request = compile_request_params(*args, &block)
         | 
| 8 | 
            +
                  log_request(request)
         | 
| 9 | 
            +
                  resource.request(request)
         | 
| 10 | 
            +
                rescue Excon::Errors::BadRequest => ex
         | 
| 11 | 
            +
                  raise ClientError, ex.response.body
         | 
| 12 | 
            +
                rescue Excon::Errors::Unauthorized => ex
         | 
| 13 | 
            +
                  raise UnauthorizedError, ex.response.body
         | 
| 14 | 
            +
                rescue Excon::Errors::NotFound => ex
         | 
| 15 | 
            +
                  raise NotFoundError, ex.response.body
         | 
| 16 | 
            +
                rescue Excon::Errors::Conflict => ex
         | 
| 17 | 
            +
                  raise ConflictError, ex.response.body
         | 
| 18 | 
            +
                rescue Excon::Errors::InternalServerError => ex
         | 
| 19 | 
            +
                  raise ServerError, ex.response.body
         | 
| 20 | 
            +
                rescue Excon::Errors::Timeout => ex
         | 
| 21 | 
            +
                  raise TimeoutError, ex.message
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              class Container
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def archive_get(path = '/', &blk)
         | 
| 29 | 
            +
                  query = { 'path' => path }
         | 
| 30 | 
            +
                  connection.get(path_for(:archive), query, response_block: blk)
         | 
| 31 | 
            +
                  self
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def archive_head(path = '/', &blk)
         | 
| 35 | 
            +
                  query = { 'path' => path }
         | 
| 36 | 
            +
                  response = connection.raw_request(:head, path_for(:archive), query, response_block: blk)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  return if response.nil?
         | 
| 39 | 
            +
                  return if response.headers.empty?
         | 
| 40 | 
            +
                  return unless response.headers.key?('X-Docker-Container-Path-Stat')
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  ContainerPathStat.new(response.headers['X-Docker-Container-Path-Stat'])
         | 
| 43 | 
            +
                rescue Docker::Error::NotFoundError
         | 
| 44 | 
            +
                  nil
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def archive_put(path = '/', overwrite: false, &blk)
         | 
| 48 | 
            +
                  headers = { 'Content-Type' => 'application/x-tar' }
         | 
| 49 | 
            +
                  query   = { 'path' => path, 'noOverwriteDirNonDir' => overwrite }
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  output = StringIO.new
         | 
| 52 | 
            +
                  blk.call(output)
         | 
| 53 | 
            +
                  output.rewind
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  connection.put(path_for(:archive), query, headers: headers, body: output)
         | 
| 56 | 
            +
                  self
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
              class ContainerPathStat
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def initialize(definition)
         | 
| 64 | 
            +
                  @data = JSON.parse(Base64.decode64(definition))
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def link_target
         | 
| 68 | 
            +
                  @data.fetch('linkTarget')
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def method_missing(method_name, *method_args, &blk)
         | 
| 72 | 
            +
                  if mode.respond_to?(method_name)
         | 
| 73 | 
            +
                    mode.public_send(method_name, *method_args, &blk)
         | 
| 74 | 
            +
                  else
         | 
| 75 | 
            +
                    super
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def mode
         | 
| 80 | 
            +
                  @mode ||= UniversalFileMode.new(@data.fetch('mode'))
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def mtime
         | 
| 84 | 
            +
                  @mtime ||= DateTime.parse(@data.fetch('mtime'))
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                def name
         | 
| 88 | 
            +
                  @data.fetch('name')
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def respond_to?(method_name)
         | 
| 92 | 
            +
                  mode.respond_to?(method_name) || super
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                def size
         | 
| 96 | 
            +
                  @data.fetch('size')
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
              end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
              # Go implementation of cross-system file modes: https://golang.org/pkg/os/#FileMode
         | 
| 102 | 
            +
              class UniversalFileMode
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                BIT_FIELDS = [
         | 
| 105 | 
            +
                  {directory:        'd'},
         | 
| 106 | 
            +
                  {append_only:      'a'},
         | 
| 107 | 
            +
                  {exclusive:        'l'},
         | 
| 108 | 
            +
                  {temporary:        'T'},
         | 
| 109 | 
            +
                  {link:             'L'},
         | 
| 110 | 
            +
                  {device:           'D'},
         | 
| 111 | 
            +
                  {named_pipe:       'p'},
         | 
| 112 | 
            +
                  {socket:           'S'},
         | 
| 113 | 
            +
                  {setuid:           'u'},
         | 
| 114 | 
            +
                  {setgid:           'g'},
         | 
| 115 | 
            +
                  {character_device: 'c'},
         | 
| 116 | 
            +
                  {sticky:           't'}
         | 
| 117 | 
            +
                ]
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def self.bit_for(name)
         | 
| 120 | 
            +
                  32 - 1 - BIT_FIELDS.index { |field| field.keys.first == name }
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                def self.flags
         | 
| 124 | 
            +
                  BIT_FIELDS.map { |field| field.keys.first }
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                def self.file_mode_mask
         | 
| 128 | 
            +
                  0777
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                def self.short_flag_for(name)
         | 
| 132 | 
            +
                  BIT_FIELDS.find { |field| field.keys.first == name }.values.first
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                def self.type_mode_mask
         | 
| 136 | 
            +
                  value_for(:directory) | value_for(:link) | value_for(:named_pipe) | value_for(:socket) | value_for(:device)
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                def self.value_for(name)
         | 
| 140 | 
            +
                  1 << bit_for(name)
         | 
| 141 | 
            +
                end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                def initialize(value)
         | 
| 144 | 
            +
                  @value = value
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                def file_mode
         | 
| 148 | 
            +
                  (@value & self.class.file_mode_mask)
         | 
| 149 | 
            +
                end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                def flags
         | 
| 152 | 
            +
                  self.class.flags.select { |name| send("#{name}?") }
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                def regular?
         | 
| 156 | 
            +
                  (@value & self.class.type_mode_mask) == 0
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                def short_flags
         | 
| 160 | 
            +
                  flags.map { |flag| self.class.short_flag_for(flag) }
         | 
| 161 | 
            +
                end
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                def to_s
         | 
| 164 | 
            +
                  short_flags.join
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                flags.each do |name|
         | 
| 168 | 
            +
                  define_method("#{name}?") do
         | 
| 169 | 
            +
                    bit_value = self.class.value_for(name)
         | 
| 170 | 
            +
                    (@value & bit_value) == bit_value
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
                end
         | 
| 173 | 
            +
             | 
| 174 | 
            +
              end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
            end
         | 
| @@ -0,0 +1,65 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Drydock
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              def self.banner
         | 
| 5 | 
            +
                "Drydock v#{Drydock.version}"
         | 
| 6 | 
            +
              end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              def self.build(opts = {}, &blk)
         | 
| 9 | 
            +
                Project.new(opts).tap do |project|
         | 
| 10 | 
            +
                  dryfile, dryfilename = yield
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  Dir.chdir(File.dirname(dryfilename))
         | 
| 13 | 
            +
                  Drydock.logger.info("Working directory set to #{Dir.pwd}")
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  begin
         | 
| 16 | 
            +
                    catch :done do
         | 
| 17 | 
            +
                      project.instance_eval(dryfile, dryfilename)
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                  rescue => e
         | 
| 20 | 
            +
                    Drydock.logger.error("Error processing #{dryfilename}:")
         | 
| 21 | 
            +
                    Drydock.logger.error(message: "#{e.class}: #{e.message}")
         | 
| 22 | 
            +
                    e.backtrace.each do |backtrace|
         | 
| 23 | 
            +
                      Drydock.logger.debug(message: "#{backtrace}", indent: 1)
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
                  ensure
         | 
| 26 | 
            +
                    Drydock.logger.info("Cleaning up")
         | 
| 27 | 
            +
                    project.finalize!
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
              def self.build_on_chain(chain, opts = {}, &blk)
         | 
| 33 | 
            +
                Project.new(opts.merge(chain: chain)).tap do |project|
         | 
| 34 | 
            +
                  project.instance_eval(&blk) if blk
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
              def self.from(repo, opts = {}, &blk)
         | 
| 39 | 
            +
                opts = opts.clone
         | 
| 40 | 
            +
                tag  = opts.delete(:tag, 'latest')
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                build(opts).tap do |project|
         | 
| 43 | 
            +
                  project.from(repo, tag)
         | 
| 44 | 
            +
                  yield project
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
              def self.logger
         | 
| 49 | 
            +
                @logger ||= Logger.new(File.new('/dev/null', 'w+'))
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              def self.logger=(logger)
         | 
| 53 | 
            +
                @logger = logger
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              def self.using(project)
         | 
| 57 | 
            +
                raise NotImplementedError, "TODO(rpasay)"
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              def self.version
         | 
| 61 | 
            +
                version_file = File.join(File.dirname(__FILE__), '..', '..', 'VERSION')
         | 
| 62 | 
            +
                File.exist?(version_file) ? File.read(version_file).chomp : ""
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            end
         | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Drydock
         | 
| 3 | 
            +
              class FileManager
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def self.find(path, ignorefile, prepend_path: false, recursive: true)
         | 
| 6 | 
            +
                  path = path.sub(%r{/$}, '')
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  [].tap do |results|
         | 
| 9 | 
            +
                    ::Find.find(path) do |subpath|
         | 
| 10 | 
            +
                      subpath = subpath.sub(%r{^#{path}/}, '')
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                      Find.prune if ignorefile.match?(subpath)
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                      if File.directory?(subpath)
         | 
| 15 | 
            +
                        Find.prune if path != subpath && !recursive
         | 
| 16 | 
            +
                      elsif prepend_path
         | 
| 17 | 
            +
                        results << File.join(path, subpath)
         | 
| 18 | 
            +
                      else
         | 
| 19 | 
            +
                        results << subpath
         | 
| 20 | 
            +
                      end
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
| @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            module Drydock
         | 
| 2 | 
            +
              module Formatters
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                DELIMITER_PATTERN = /(\d)(?=(\d\d\d)+(?!\d))/
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def self.number(value, delimiter: ',', separator: '.')
         | 
| 7 | 
            +
                  integers, decimals = value.to_s.split('.')
         | 
| 8 | 
            +
                  integers.gsub!(DELIMITER_PATTERN) { |digits| "#{digits}#{delimiter}" }
         | 
| 9 | 
            +
                  [integers, decimals].compact.join(separator)
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
            end
         | 
| @@ -0,0 +1,61 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Drydock
         | 
| 3 | 
            +
              class IgnorefileDefinition
         | 
| 4 | 
            +
                class Rule < Struct.new(:pattern, :exclude)
         | 
| 5 | 
            +
                  alias_method :exclude?, :exclude
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def match?(test)
         | 
| 8 | 
            +
                    File.fnmatch?(pattern, test)
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                extend Forwardable
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def_delegators :@rules, :count, :length, :size
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def initialize(file_or_filename, dotfiles: false)
         | 
| 17 | 
            +
                  @dotfiles = dotfiles
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  if file_or_filename.respond_to?(:readlines)
         | 
| 20 | 
            +
                    patterns = Array(file_or_filename.readlines)
         | 
| 21 | 
            +
                  else
         | 
| 22 | 
            +
                    patterns = File.exist?(file_or_filename) ? File.readlines(file_or_filename) : []
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  @rules = patterns.map do |pattern|
         | 
| 26 | 
            +
                    pattern = pattern.chomp
         | 
| 27 | 
            +
                    if pattern.start_with?('!')
         | 
| 28 | 
            +
                      Rule.new(pattern.slice(1..-1), true)
         | 
| 29 | 
            +
                    else
         | 
| 30 | 
            +
                      Rule.new(pattern, false)
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def match?(filename)
         | 
| 36 | 
            +
                  return false if excludes?(filename)
         | 
| 37 | 
            +
                  return true if includes?(filename)
         | 
| 38 | 
            +
                  return true if is_dotfile?(filename)
         | 
| 39 | 
            +
                  return false
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                private
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def excludes?(filename)
         | 
| 45 | 
            +
                  @rules.select { |rule| rule.exclude? }.any? do |rule|
         | 
| 46 | 
            +
                    rule.match?(filename)
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def includes?(filename)
         | 
| 51 | 
            +
                  @rules.select { |rule| !rule.exclude? }.any? do |rule|
         | 
| 52 | 
            +
                    rule.match?(filename)
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def is_dotfile?(filename)
         | 
| 57 | 
            +
                  @dotfiles && filename.start_with?('.') && filename.size > 1
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
            end
         | 
| @@ -0,0 +1,50 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Drydock
         | 
| 3 | 
            +
              class ImageRepository
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                class <<self
         | 
| 6 | 
            +
                  include Enumerable
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                @image_cache = {}
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def self.all
         | 
| 12 | 
            +
                  image_count = @image_cache.size
         | 
| 13 | 
            +
                  Docker::Image.all(all: 1).map do |image|
         | 
| 14 | 
            +
                    @image_cache[image.id] ||= Docker::Image.get(image.id)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                ensure
         | 
| 17 | 
            +
                  delta_count = @image_cache.size - image_count
         | 
| 18 | 
            +
                  if delta_count > 0
         | 
| 19 | 
            +
                    Drydock.logger.info(message: "Loaded metadata for #{delta_count} images from docker cache")
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def self.dangling
         | 
| 24 | 
            +
                  filters = {dangling: ["true"]}
         | 
| 25 | 
            +
                  Docker::Image.all(filters: filters.to_json)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def self.each(&blk)
         | 
| 29 | 
            +
                  self.all.each(&blk)
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def self.find_by_config(config)
         | 
| 33 | 
            +
                  base_image = config['Image']
         | 
| 34 | 
            +
                  candidates = self.select_by_config(config)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  possibles = candidates.select do |image|
         | 
| 37 | 
            +
                    image.info['Parent'] == base_image || image.info['ContainerConfig']['Image'] == base_image
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  possibles.sort_by { |image| image.info['Created'] }.last
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def self.select_by_config(config)
         | 
| 44 | 
            +
                  # we want to look at 'ContainerConfig', because we're interesting in how
         | 
| 45 | 
            +
                  # the image was built, not how the image will run
         | 
| 46 | 
            +
                  self.select { |image| config == ContainerConfig.from(image.info['ContainerConfig']) }
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
            end
         |