Skip to content
This repository has been archived by the owner on Apr 6, 2021. It is now read-only.

Commit

Permalink
Merge pull request #1 from shantytown/docker-plugin
Browse files Browse the repository at this point in the history
Create docker plugin
  • Loading branch information
nathankleyn committed Oct 6, 2015
2 parents 179d034 + 94a9368 commit 9187a1c
Show file tree
Hide file tree
Showing 36 changed files with 1,636 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.gem
coverage
Gemfile.lock
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require: rubocop-rspec

LineLength:
Max: 120
17 changes: 17 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
sudo: false
language: ruby
rvm:
- 2.2.3

# We have to install Rust manually, as Travis does not yet allow multiligual
# builds. We install the latest stable Rust build.
before_install:
- mkdir ~/rust-installer
- curl -sL https://static.rust-lang.org/rustup.sh -o ~/rust-installer/rustup.sh
- sh ~/rust-installer/rustup.sh --prefix=~/rust --spec=stable -y --disable-sudo 2> /dev/null
- export PATH=~/rust/bin:$PATH
- export LD_LIBRARY_PATH=~/rust/lib:$LD_LIBRARY_PATH
- rustc --version
- cargo --version

script: bundle exec shanty test
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source 'https://rubygems.org'

gemspec
gem 'shanty', git: 'https://github.com/shantytown/shanty'
4 changes: 4 additions & 0 deletions Shantyconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require 'shanty/plugins/bundler_plugin'
require 'shanty/plugins/rspec_plugin'
require 'shanty/plugins/rubocop_plugin'
require 'shanty/plugins/rubygem_plugin'
8 changes: 8 additions & 0 deletions lib/shanty_docker_plugin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'shanty_docker_plugin/docker_credentials'
require 'shanty_docker_plugin/docker_plugin'
require 'shanty_docker_plugin/error'
require 'shanty_docker_plugin/image_wrapper'
# Public: shanty docker plugin
module DockerPlugin
DOCKER_FILE = 'Dockerfile'
end
45 changes: 45 additions & 0 deletions lib/shanty_docker_plugin/docker_credentials.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require 'base64'
require 'docker'
require 'json'
require 'uri'

module DockerPlugin
# Public: Set up credentials from ~/.dockercfg
class DockerCredentials
def initialize(dockercfg_file = "#{Dir.home}/.dockercfg")
@dockercfg_file = dockercfg_file
end

def auth!(registry)
Docker.creds = find_credentials(registry)
end

private

def find_credentials(registry)
(credentials[registry] || credentials['index.docker.io'] || {}).to_h
end

def credentials
@credentials ||= read_dockercfg.each_with_object({}) do |(k, v), acc|
username, password = decode_credentials(v['auth'])
acc[registry(k)] = { username: username, password: password, email: v['email'] }
end
end

def registry(url)
host = url
host = URI(url).host if url.start_with?('http')
host
end

def decode_credentials(base64)
Base64.decode64(base64).split(':')
end

def read_dockercfg
return {} unless File.exist?(@dockercfg_file)
JSON.parse(File.open(@dockercfg_file, 'rb').read)
end
end
end
45 changes: 45 additions & 0 deletions lib/shanty_docker_plugin/docker_file.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require 'erubis'
require 'fileutils'
require 'shanty/env'
require 'shanty_docker_plugin'

module DockerPlugin
# Public: Process a dockerfile as an ERB template
class DockerFile
include Shanty::Logger

attr_reader :relative_output_file, :output_file

# Public: initialize the docker file
#
# base_dir - the directory where the docker file is located
# registry - the docker registry to inject into the template
# tag - the build tag to inject into the template
# artifacts - an array of relative paths to artifacts built by dependencies
# build_dir - the directory to output the calculated dockerfile
def initialize(base_dir, registry, tag, build_dir, artifacts = nil)
@base_dir = base_dir
@registry = registry
@tag = tag
@artifacts = artifacts
@output_dir = File.join(base_dir, build_dir)
@relative_output_file = File.join(build_dir, DOCKER_FILE)
@output_file = File.join(base_dir, @relative_output_file)
@docker_file = File.join(base_dir, DOCKER_FILE)
end

# Public: write the docker file from the template
def write!
FileUtils.mkdir_p(@output_dir)
File.write(@output_file, contents)
end

# Public: process the contents of a docker file
def contents
fail("Docker file does not exist at #{@docker_file}") unless File.exist?(@docker_file)
@contents ||= Erubis::Eruby.new(File.read(@docker_file)).result(registry: @registry,
tag: @tag,
artifacts: @artifacts)
end
end
end
48 changes: 48 additions & 0 deletions lib/shanty_docker_plugin/docker_output_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require 'shanty_docker_plugin/error'
require 'shanty/logger'

module DockerPlugin
# Methods for parsing the output from docker
module DockerOutputParser
extend Shanty::Logger

def self.parse_chunk(chunk)
parse_output(chunk) do |output|
if output.respond_to?(:key?)
fail output['error'] if output.key?('error')
logger.info(output['stream']) if output.key?('stream')
end
end
end

private_class_method

# Private: safely parse the output from docker
# logs a warning if the output could not be parsed
#
# body - the body of the output to process
# &block - the block to call with the parsed output
def self.parse_output(body, &block)
parse_output!(body, &block)
rescue Docker::Error::UnexpectedResponseError => e
logger.warn("Could not parse output from docker: #{e}")
logger.warn(body)
end

# Private: parse the output from docker
#
# body - the body of the output to process
# block - the block to call with the parsed output
def self.parse_output!(body, &block)
if body.include?('}{')
body.split('}{').each do |line|
line = "{#{line}" unless line =~ /\A{/
line = "#{line}}" unless line =~ /}\z/
yield(Docker::Util.parse_json(line))
end
else
body.each_line { |line| block.call(Docker::Util.parse_json(line)) }
end
end
end
end
103 changes: 103 additions & 0 deletions lib/shanty_docker_plugin/docker_plugin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
require 'shanty/plugin'
require 'shanty/project'
require 'shanty_docker_plugin/docker_credentials'
require 'shanty_docker_plugin/image_resolution_fallback'
require 'rugged'

module DockerPlugin
# Shanty docker plugin
class DockerPlugin < Shanty::Plugin
NAME_OPTION = :docker_project_name
DEPENDENCY_OPTION = :docker_dependency

def initialize(*args)
super(*args)
@docker_crendentials = DockerCredentials.new
end

provides_config NAME_OPTION
provides_config :registry
provides_config :tag
provides_config :output_dir, 'build'
provides_config :fallback_tag, 'latest'
subscribe :build, :on_build
subscribe :deploy, :on_deploy
provides_projects :resolve_projects
description 'Discovers and builds Docker projects'

def self.resolve_projects(env)
project_dependencies(find_projects(env))
end

private_class_method

def self.find_projects(env)
env.file_tree.glob("**/#{DOCKER_FILE}").each_with_object({}) do |f, acc|
project_path = File.dirname(f)
project = find_or_create_project(project_path, env)

project_name(project)
project.config[DEPENDENCY_OPTION] = project_dependency(f)

acc[project.config[NAME_OPTION]] = project
end
end

def self.project_dependencies(projects)
projects.values.each do |project|
project.add_parent(projects[project.config[DEPENDENCY_OPTION]]) unless project.config[DEPENDENCY_OPTION].nil?
end
end

def self.project_name(project)
project.config[NAME_OPTION] = File.basename(project.path) if project.config[NAME_OPTION].empty?
end

def self.project_dependency(file)
return unless (match = File.open(file, 'rb').read.match(%r{(FROM|from)\s+<%=\s*registry\s*%>/(?<name>\S+):\S+}))
match[:name]
end

def on_build
if project.changed?
build_image
else
@docker_crendentials.auth!(image_wrapper.registry)
ImageResolutionFallback.pull!(image_wrapper, config[:fallbacktag])
end
end

def on_deploy
@docker_crendentials.auth!(image_wrapper.registry)
image_wrapper.image.push
end

private

def tag
config[:tag] || git_tag || 'latest'
end

def git_tag
@git_tag ||= GitTag.current_branch(env.root)
end

def image_wrapper
ImageWrapper.new(project.config[NAME_OPTION], tag, config[:registry])
end

def build_image
artifacts = ParentArtifacts.copy(project, config[:output_dir])

image_wrapper.build(project.path, docker_file(project.path, artifacts))
end

def docker_file(project_path, artifacts = nil)
DockerFile.new(project_path,
config[:registry],
tag,
config[:output_dir],
artifacts)
end
end
end
5 changes: 5 additions & 0 deletions lib/shanty_docker_plugin/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module DockerPlugin
module Error
class PullError < StandardError; end
end
end
25 changes: 25 additions & 0 deletions lib/shanty_docker_plugin/git_tag.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require 'shanty/logger'
require 'rugged'

module DockerPlugin
# Public: methods for getting the docker tag from
# git branch
module GitTag
extend Shanty::Logger

module_function

# Public: get the current branch from a git repo
#
# repo_root - the git root directory
#
# Returns the current branch
def current_branch(repo_root)
repo = Rugged::Repository.new(repo_root)
repo.head.name.sub(%r{^refs/heads/}, '')
rescue
logger.debug('Could not resolve current branch')
nil
end
end
end
22 changes: 22 additions & 0 deletions lib/shanty_docker_plugin/image_resolution_fallback.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'shanty_docker_plugin/error'
require 'shanty/logger'

module DockerPlugin
# Methods for falling back to a different tag when resolving images
module ImageResolutionFallback
extend Shanty::Logger

module_function

def pull!(image, fallback_tag)
image.pull!
rescue Error::PullError
logger.warn(
"Could not find image #{image.name} with tag #{image.tag}, trying with #{fallback_tag}"
)
new_image = ImageWrapper.new(image.name, fallback_tag, image.registry)
new_image.pull!
new_image.add_tag!(image.tag)
end
end
end
Loading

0 comments on commit 9187a1c

Please sign in to comment.