├── .gitignore ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── dacker ├── dacker.gemspec ├── lib ├── dacker.rb └── dacker │ ├── container.rb │ ├── container_deployer.rb │ ├── file_copier.rb │ ├── file_loader.rb │ ├── file_parser.rb │ ├── host.rb │ ├── installer.rb │ ├── logger.rb │ ├── orchestrator.rb │ ├── shell_runner.rb │ └── version.rb ├── spec ├── dacker │ └── container_spec.rb ├── spec_helper.rb └── support │ └── docker_build_example │ └── Dockerfile └── templates ├── Vagrantfile └── rails ├── Dackerfile.yml ├── Dockerfile └── dacker └── templates └── vhost /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | /Dackerfile.yml 16 | /Dackerfile.yml.old 17 | /dacker/ 18 | /dacker.old/ 19 | /Vagrantfile 20 | /Vagrantfile.old 21 | /Dockerfile 22 | /Dockerfile.old 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in dacker.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'rspec', cmd: "bundle exec rspec" do 2 | # watch /lib/ files 3 | watch(%r{^lib/(.+).rb$}) do |m| 4 | "spec/#{m[1]}_spec.rb" 5 | end 6 | 7 | # watch /spec/ files 8 | watch(%r{^spec/(.+).rb$}) do |m| 9 | "spec/#{m[1]}.rb" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ben Dixon 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dacker 2 | 3 | Dacker is a Docker orchestration tool written in Ruby. It works across multiple hosts and is designed to be used for managing both development and production environments. 4 | 5 | ## Dacker v Compose 6 | 7 | On the surface Dacker has a lot in common with [Compose](http://docs.docker.com/compose/). It allows you to orchestrate containers based on a yaml file definition. In practice the primary purpose of Dacker was to allow us to orchestrate containers directly from a Rails application by passing around standard Ruby hashes. YAML file deployment was a welcome bonus since YAML files are easily loaded into standard Ruby hashes. 8 | 9 | If you just need YAML file container orchestration, you're almost certainly better off using Compose as it has far broader functionality. If you're looking to directly orchestrate containers from Ruby code, Dacker may be useful to you. 10 | 11 | ## Why Dacker Exists 12 | 13 | Dacker began as an internal tool at [Make It With Code](http://www.makeitwithcode.com). Specifically we needed to deploy a NodeJS application in a container, whenever a user signed up to the Rails application. The Rails application needed to manage this deployment. When an existing user logged in, we needed to be able to check if their container was running and if not re-start it. 14 | 15 | We built Dacker because the above meant we had several requirements, not met by existing orchestration tools: 16 | 17 | * An easy way to embed container lifecycle management into a Rails application and reason in terms of "container state" rather than specific Docker API calls 18 | * A single toolchain for both development environments and production deployments 19 | * Full support for deploying to multiple hosts without a requirement to publicly expose the Docker Daemons HTTP API 20 | * An easy method of managing production infrastructure without requiring any additional server side daemons or central orchestration servers 21 | * Very quick deployment of "standard" Ruby (Rails or Sinatra) applications and associated stacks which could co-exist and scale across a shared pseudo cluster of standard nodes 22 | 23 | ## Example Usage (Rails) 24 | 25 | This example assumes a Rails application using Postgres as a database but should be generally applicable to web applications. Vagrant is required for local development so make sure you've got an up to date version installed before starting . 26 | 27 | Begin by adding the `dacker` gem to your `Gemfile` and running `bundle`. 28 | 29 | In the root of the project execute `bundle exec dacker install`. This will generate a simple example configuration, including a `Vagrantfile` for local development. 30 | 31 | The most important file here is the `Dackerfile.yml` which uses a [Fig](www.fig.sh) like syntax for defining containers and environments. 32 | 33 | The default Rails Dackerfile looks like this: 34 | 35 | ```yaml 36 | vagrant: &VAGRANT 37 | host: 192.168.50.60 38 | user: vagrant 39 | password: vagrant 40 | 41 | development: 42 | rails_app: 43 | build: . 44 | ports: 45 | - "3000:3000" 46 | environment: 47 | - RAILS_ENV=development 48 | - PG_HOST=192.168.50.60 49 | - PG_USERNAME=postgres 50 | volumes: 51 | - /vagrant:/app 52 | deploy: 53 | name: web1 54 | signal: SIGTERM 55 | container: 56 | - delete 57 | - build 58 | - create 59 | - start 60 | order: 2 61 | <<: *VAGRANT 62 | 63 | load_balancer: 64 | image: nginx 65 | volumes: 66 | - /home/vagrant/vhosts:/etc/nginx/conf.d 67 | ports: 68 | - "80:80" 69 | deploy: 70 | name: lb1 71 | files: 72 | - /home/vagrant/vhosts/test_app.conf:dacker/templates/vhost 73 | signal: HUP 74 | order: 1 75 | <<: *VAGRANT 76 | 77 | database: 78 | image: postgres 79 | volumes: 80 | - /home/vagrant/pg_data:/var/lib/postgresql/data 81 | ports: 82 | - 5432:5432 83 | deploy: 84 | name: pg1 85 | order: 0 86 | <<: *VAGRANT 87 | ``` 88 | 89 | This defines three containers for our environment, an Nginx load balancer, a rails application and a postgresql database server. It also defines the volumes for these containers for persistence and the ports to be exposed. If the idea of data volumes and exposing ports is new to you don't worry, just head over to [the interactive docker tutorial](https://www.docker.com/tryit/) then come back here! 90 | 91 | Notice that this is a standard YAML file, so you can use anchors and aliases in exactly the same way they're used in something like the Rails `database.yml`. 92 | 93 | Bring up the Vagrant node with `vagrant up`. This a lightweight VM running only the Docker daemon. You will be prompted for your sudo password, this is required to setup NFS shares which offer far better performance than the Vagrant defaults. 94 | 95 | Ensure the `pg` gem is present in your Gemfile and then modify your `config/database.yml` to source credentials from the environment as follows: 96 | 97 | ```yaml 98 | default: &default 99 | adapter: postgresql 100 | pool: 5 101 | timeout: 5000 102 | host: <%= ENV['PG_HOST'] %> 103 | username: <%= ENV['PG_USERNAME'] %> 104 | password: <%= ENV['PG_PASSWORD'] %> 105 | 106 | development: 107 | <<: *default 108 | database: application_name_development 109 | 110 | test: 111 | <<: *default 112 | database: application_name_test 113 | 114 | production: 115 | <<: *default 116 | database: application_name_production 117 | ``` 118 | 119 | Note that we set these environment variables in the `environment` section of the `rails_app` container in the `Dackerfile`. 120 | 121 | Execute `bundle exec dacker deploy` and wait. The first time you run this it may take a while as it has to download several images and build the Rails app image from scratch. Subsequent usage will be much faster. 122 | 123 | Visit 192.168.50.60 in your browser. If all's well you will see your Rails app! 124 | 125 | The default configuration mounts the root of the project in `/vagrant` which is in turn mounted in /app of the `rails_app` docker container. This means that changes made locally in development will be reflected immediately as per usual, without needing to re-deploy. 126 | 127 | ## Executing Commands in the Rails Environment 128 | 129 | Dacker provides a simple interface for running commands within your applications containers. 130 | 131 | To execute a command in the `rails_app` container, as defined in the `Dackerfile`, use: 132 | 133 | ```bash 134 | bundle exec dacker execute rails_app "SOMECOMMAND" 135 | ``` 136 | 137 | For example to create your database: 138 | 139 | ```bash 140 | bundle exec dacker execute rails_app "rake db:create" 141 | ``` 142 | 143 | And to start a Rails console: 144 | 145 | ```bash 146 | bundle exec dacker execute rails_app "rails console" 147 | ``` 148 | 149 | You can even start a standard shell with: 150 | 151 | ```bash 152 | bundle exec dacker execute rails_app "bash" 153 | ``` 154 | 155 | Remember though, each of these commands runs in an entirely isolated environment, so no files are persisted. The exception to this is files written to `/app` in development since this is a shared folder on your local filesystem (the project root). 156 | 157 | ## Known Issues 158 | 159 | When building the container from a local Dockerfile after doing `dacker deploy`, you'll sometimes see an error like: `error getting container from driver devicemapper`. This appears to be due to some sort of race condition when unmounting/ mounting. Generally just re-running the deploy will resolve it, it's currently unclear exactly why this occurs. 160 | 161 | ## Specs 162 | 163 | Requires a live Docker host to run against (eventually this will use VCR). The Docker API should be accessible on localhost, port 5000. The easiest way is to fire up a suitable Vagrant box and then establish an SSH tunnel, e.g. `ssh -L 5000:127.0.0.1:2375 -N deploy@192.168.50.31`. This host should have the `ubuntu` base image already pulled (e.g. `docker pull ubuntu`). 164 | 165 | ## Contributing 166 | 167 | 1. Fork it ( https://github.com/[my-github-username]/dacker/fork ) 168 | 2. Create your feature branch (`git checkout -b my-new-feature`) 169 | 3. Commit your changes (`git commit -am 'Add some feature'`) 170 | 4. Push to the branch (`git push origin my-new-feature`) 171 | 5. Create a new Pull Request 172 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | require 'bundler/gem_tasks' 3 | 4 | # Default directory to look in is `/specs` 5 | # # Run with `rake spec` 6 | RSpec::Core::RakeTask.new(:spec) do |task| 7 | task.rspec_opts = ['--color', '--format', 'nested'] 8 | end 9 | 10 | task :default => :spec 11 | 12 | -------------------------------------------------------------------------------- /bin/dacker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib = File.expand_path(File.dirname(__FILE__) + '/../lib') 4 | $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib) 5 | 6 | require 'thor' 7 | require 'dacker' 8 | 9 | class DackerRunner < Thor 10 | desc "deploy", "Unleashes homer to feed on your dotfiles" 11 | method_option :env, :desc => "The environment to deploy to", default: "development" 12 | method_option :dackerfile, desc: "File from which configuration should be loaded", default: "Dackerfile.yml" 13 | def deploy 14 | ::Dacker::Orchestrator.new( 15 | dackerfile: options[:dackerfile], 16 | env: options[:env] 17 | ).deploy! 18 | end 19 | 20 | desc "run CONTAINER CMD", "run an arbitary CMD in CONTAINER" 21 | method_option :dackerfile, desc: "File from which configuration should be loaded", default: "Dackerfile.yml" 22 | def execute(container, cmd) 23 | Dacker::ShellRunner.new( 24 | definition: Dacker::FileLoader.new( 25 | path: options[:dackerfile] 26 | ).content[container] 27 | ).cmd(cmd) 28 | end 29 | 30 | desc "install", "create a basic dacker structure" 31 | def install 32 | ::Dacker::Installer.new.install 33 | end 34 | end 35 | DackerRunner.start 36 | -------------------------------------------------------------------------------- /dacker.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'dacker/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "dacker" 8 | spec.version = Dacker::VERSION 9 | spec.authors = ["Ben Dixon"] 10 | spec.email = ["ben@talkingquickly.co.uk"] 11 | spec.summary = %q{Multi host Docker Orchestration tool} 12 | spec.homepage = "https://github.com/TalkingQuickly/dacker" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency 'docker-api', '~> 1.14.0' 21 | spec.add_dependency 'net-ssh', '~> 2.9.1' 22 | spec.add_dependency 'net-scp', '~> 1.2.1' 23 | spec.add_dependency 'net-ssh-gateway', '~> 1.2.0' 24 | spec.add_dependency 'colorize', '~> 0.7.3' 25 | spec.add_dependency 'thor', '~> 0.19.1' 26 | 27 | spec.add_development_dependency "bundler", "~> 1.7" 28 | spec.add_development_dependency "rake", "~> 10.0" 29 | spec.add_development_dependency "rspec" 30 | spec.add_development_dependency "rspec-nc" 31 | spec.add_development_dependency "guard" 32 | spec.add_development_dependency "guard-rspec" 33 | spec.add_development_dependency "pry" 34 | spec.add_development_dependency "pry-remote" 35 | spec.add_development_dependency "pry-nav" 36 | end 37 | -------------------------------------------------------------------------------- /lib/dacker.rb: -------------------------------------------------------------------------------- 1 | require "dacker/version" 2 | require "dacker/container" 3 | require "dacker/container_deployer" 4 | require "dacker/file_copier" 5 | require "dacker/file_loader" 6 | require "dacker/file_parser" 7 | require "dacker/host" 8 | require "dacker/logger" 9 | require "dacker/orchestrator" 10 | require "dacker/shell_runner" 11 | require "dacker/installer" 12 | 13 | Excon.defaults[:write_timeout] = 1000 14 | Excon.defaults[:read_timeout] = 1000 15 | 16 | module Dacker 17 | def self.root 18 | File.dirname __dir__ 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/dacker/container.rb: -------------------------------------------------------------------------------- 1 | require 'docker' 2 | 3 | module Dacker 4 | class Container 5 | def initialize(options={}) 6 | # @TODO: this is really an option parser 7 | @config = FileParser.new( 8 | config: options[:config] 9 | ) 10 | @name = options[:name] 11 | @host = Host.new( 12 | host: options[:host], 13 | username: options[:username], 14 | password: options[:password] 15 | ) 16 | @image = config.image 17 | end 18 | 19 | attr_accessor :config, :host, :image, :name 20 | 21 | def container(running=true) 22 | host.containers(all: !running).select do |c| 23 | c.info["Names"].include? "/#{name}" 24 | end.first 25 | end 26 | 27 | def signal(signal) 28 | log "signalling: #{signal}" 29 | container(true).kill( 30 | signal: signal 31 | ) if container(true) 32 | end 33 | 34 | def start 35 | log "starting container" 36 | container(false).start( 37 | { 38 | "Binds" => config.binds, 39 | "PortBindings" => config.port_bindings 40 | } 41 | ) if exists? 42 | end 43 | 44 | def build 45 | return nil unless config.build? 46 | log "building image from dir: #{config.build}" 47 | the_image ||= ::Docker::Image.build_from_dir( 48 | config.build, 49 | {}, 50 | host.docker, 51 | {} 52 | ) do |output| 53 | #puts " build:: #{output}" 54 | end 55 | @image = the_image.id 56 | the_image.tag(repo: name) 57 | log "build complete" 58 | end 59 | 60 | def pull 61 | log "pulling image: #{image}" 62 | ::Docker::Image.create( 63 | { 64 | fromImage: image 65 | }, 66 | nil, 67 | host.docker 68 | ) 69 | log "pull completed" 70 | end 71 | 72 | def create 73 | pull if !config.build? 74 | log "creating container" 75 | Docker::Container.create( 76 | { 77 | "Env" => config.env, 78 | "Image" => image, 79 | "ExposedPorts" => config.exposed_ports, 80 | "name" => name 81 | }, 82 | host.docker 83 | ) 84 | log "container created" 85 | end 86 | 87 | def stop 88 | container.stop if running? 89 | end 90 | 91 | def restart 92 | container.restart if exists? 93 | end 94 | 95 | def delete 96 | container(false).delete(force: true) if exists? 97 | end 98 | 99 | def exists? 100 | !container(false).nil? 101 | end 102 | 103 | def container_id 104 | container(false).info["id"] 105 | end 106 | 107 | def running? 108 | !container(true).nil? 109 | end 110 | 111 | def log(message, color=:green) 112 | Logger.log("#{name}: #{message}", color) 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/dacker/container_deployer.rb: -------------------------------------------------------------------------------- 1 | module Dacker 2 | class ContainerDeployer 3 | def initialize(options={}) 4 | @definition = options[:definition] 5 | end 6 | 7 | attr_accessor :definition 8 | 9 | def deploy! 10 | log("starting deploy") 11 | copy_files 12 | if create_only? && !container.running? 13 | log("create only container is not running") 14 | container.build if !container.exists? 15 | container.create if !container.exists? 16 | container.start 17 | end 18 | if signal 19 | container.signal(signal) 20 | end 21 | if changes_container? 22 | check_stop 23 | config["container"].each do |cmd| 24 | container.send(cmd) 25 | end 26 | end 27 | check_running 28 | container.host.close_port! 29 | end 30 | 31 | def copy_files 32 | FileCopier.new( 33 | config: config 34 | ).copy! 35 | end 36 | 37 | def signal 38 | config["signal"] 39 | end 40 | 41 | def check_stop 42 | if !config["container"].include?("stop") && container.running? 43 | if signal 44 | log "container still running", :yellow 45 | log "waiting 5 seconds to see if it stops", :yellow 46 | sleep(5) 47 | end 48 | log "stop command not included and container still running", :red 49 | log "either use a signal to stop the containers running process or include stop", :red 50 | end 51 | end 52 | 53 | def check_running 54 | if container.running? 55 | log "container is running!" 56 | else 57 | log "container not running", :red 58 | end 59 | end 60 | 61 | def changes_container? 62 | !create_only? 63 | end 64 | 65 | def create_only? 66 | config["container"].nil? || config["container"].empty? 67 | end 68 | 69 | def config 70 | @deploy_config ||= definition["deploy"] 71 | end 72 | 73 | def container 74 | @container ||= Container.new( 75 | config: definition, 76 | name: definition["deploy"]["name"], 77 | host: definition["deploy"]["host"], 78 | username: definition["deploy"]["user"], 79 | password: definition["deploy"]["password"] 80 | ) 81 | end 82 | 83 | def name 84 | @name ||= definition["deploy"]["name"] 85 | end 86 | 87 | def log(message, color=:green) 88 | Logger.log("#{name}: #{message}", color) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/dacker/file_copier.rb: -------------------------------------------------------------------------------- 1 | require 'net/scp' 2 | 3 | module Dacker 4 | class FileCopier 5 | def initialize(options={}) 6 | @files = options[:config]["files"] 7 | @host = options[:config]["host"] 8 | @user = options[:config]["user"] 9 | @password = options[:config]["password"] 10 | end 11 | 12 | attr_accessor :files, :host, :user, :password 13 | 14 | def copy! 15 | return unless files 16 | files.each do |file| 17 | destination = file.split(":").first 18 | source = file.split(":").last 19 | if !File.exist? source 20 | log "local file #{source} does not exist, skipping", :red 21 | return 22 | end 23 | log "copying #{source} to #{destination}" 24 | ensure_dir(destination) 25 | Net::SCP.upload!( 26 | host, 27 | user, 28 | source, 29 | destination, 30 | ssh: ssh_options 31 | ) 32 | log "copied ok" 33 | end 34 | end 35 | 36 | def ensure_dir(file) 37 | Net::SSH.start(host, user, ssh_options) do |ssh| 38 | ssh.exec "mkdir -p #{file.split("/")[0..-2].join("/")}" 39 | end 40 | end 41 | 42 | def ssh_options 43 | if password 44 | { 45 | password: password 46 | } 47 | else 48 | {} 49 | end 50 | end 51 | 52 | def log(message, color=:green) 53 | Logger.log("#{host}: #{message}", color) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/dacker/file_loader.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Dacker 4 | class FileLoader 5 | def initialize(options={}) 6 | @path = options[:path] || "Dackerfile.yml" 7 | @env = options[:env] || "development" 8 | end 9 | 10 | attr_accessor :path, :env 11 | 12 | def content 13 | @content ||= YAML.load_file(path)[env] 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/dacker/file_parser.rb: -------------------------------------------------------------------------------- 1 | module Dacker 2 | # @YODO: this is really an option parser 3 | class FileParser 4 | def initialize(options={}) 5 | @config = options[:config] 6 | end 7 | 8 | attr_accessor :config 9 | 10 | def port_bindings 11 | out = {} 12 | return out if config["ports"].nil? 13 | config["ports"].each do |port| 14 | host = port.split(":")[0] 15 | container = port.split(":")[1] 16 | out["#{container}/tcp"] = [{"HostPort" => host}] 17 | end 18 | out 19 | end 20 | 21 | def binds 22 | config["volumes"] 23 | end 24 | 25 | def env 26 | config["environment"].join(" ") if config["environment"] 27 | config["environment"] || [] 28 | end 29 | 30 | def exposed_ports 31 | out = {} 32 | return out if config["ports"].nil? 33 | config["ports"].each do |port| 34 | container = port.split(":")[1] 35 | out["#{container}/tcp"] = {} 36 | end 37 | out 38 | end 39 | 40 | def build 41 | config["build"] 42 | end 43 | 44 | def image 45 | config["image"] 46 | end 47 | 48 | def build? 49 | !build.nil? 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/dacker/host.rb: -------------------------------------------------------------------------------- 1 | require 'net/ssh' 2 | require 'net/ssh/gateway' 3 | 4 | module Dacker 5 | class Host 6 | def initialize(options={}) 7 | @host = options[:host] 8 | @port = options[:port] || 2375 9 | @username = options[:username] || 'deploy' 10 | @password = options[:password] 11 | forward_port! 12 | authenticate! 13 | end 14 | 15 | def authenticate! 16 | if (user=ENV['DOCKER_USERNAME']) && (password=ENV['DOCKER_PASSWORD']) && (email=ENV['DOCKER_EMAIL']) 17 | Docker.authenticate!( 18 | { 19 | 'username' => user, 20 | 'password' => password, 21 | 'email' => email 22 | }, 23 | docker 24 | ) 25 | end 26 | end 27 | 28 | attr_accessor :host, :port, :username, :password, :destport 29 | 30 | def containers(options) 31 | ::Docker::Container.all(options, docker) 32 | end 33 | 34 | def info 35 | ::Docker.info(docker) 36 | end 37 | 38 | def docker 39 | @docker ||= Docker::Connection.new( 40 | "tcp://127.0.0.1:#{destport}", 41 | {} 42 | ) 43 | end 44 | 45 | def gateway 46 | @gateway ||= Net::SSH::Gateway.new(host, username, gateway_options) 47 | end 48 | 49 | def gateway_options 50 | if password 51 | { 52 | password: password 53 | } 54 | else 55 | {} 56 | end 57 | end 58 | 59 | def forward_port! 60 | @destport = gateway.open('127.0.0.1', port) 61 | end 62 | 63 | def close_port! 64 | gateway.shutdown! 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/dacker/installer.rb: -------------------------------------------------------------------------------- 1 | module Dacker 2 | class Installer 3 | def initialize 4 | @variant = "rails" 5 | end 6 | 7 | attr_accessor :variant 8 | 9 | def install 10 | begin 11 | existing_file("Dackerfile.yml") 12 | existing_file("Vagrantfile") 13 | existing_file("Dockerfile") 14 | existing_folder("dacker") 15 | copy("Dackerfile.yml","Dackerfile.yml") 16 | copy("Dockerfile","Dockerfile") 17 | copy("Vagrantfile", "Vagrantfile", false) 18 | copy_directory("dacker","./dacker") 19 | rescue Errno::ENOTEMPTY 20 | log "please remove your dacker.old dir then retry", :red 21 | end 22 | end 23 | 24 | def copy_directory(source, destination) 25 | log "copying #{source} to #{destination}" 26 | FileUtils.copy_entry( 27 | File.join(template_path, source), 28 | destination 29 | ) 30 | end 31 | 32 | def copy(source, destination, use_variant=true) 33 | log "copying #{source} to #{destination}" 34 | FileUtils.cp( 35 | File.join(template_path(use_variant), source), 36 | destination 37 | ) 38 | end 39 | 40 | def existing_file(file) 41 | if File.exist? file 42 | log "backing up: #{file} to #{file}.old" 43 | File.rename(file, "#{file}.old") 44 | end 45 | end 46 | 47 | def existing_folder(folder) 48 | log "backing up: #{folder} to #{folder}.old" 49 | if File.directory? folder 50 | File.rename(folder, "#{folder}.old") 51 | end 52 | end 53 | 54 | def template_path(use_variant=true) 55 | if use_variant 56 | File.join(::Dacker.root, "templates", variant) 57 | else 58 | File.join(::Dacker.root, "templates") 59 | end 60 | end 61 | 62 | def log(message,color=:green) 63 | Logger.log("install: #{message}", color) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/dacker/logger.rb: -------------------------------------------------------------------------------- 1 | require 'colorize' 2 | 3 | module Dacker 4 | class Logger 5 | def self.log(message, color) 6 | puts " dacker: #{message}".colorize(color) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/dacker/orchestrator.rb: -------------------------------------------------------------------------------- 1 | module Dacker 2 | class Orchestrator 3 | def initialize(options={}) 4 | @dackerfile_path = options[:dackerfile] || 'Dacerfile.yml' 5 | @env = options[:env] 6 | @dacker = options[:dacker] || dackerfile 7 | log "No configuration found for environment: #{env}", :red unless dacker 8 | end 9 | 10 | attr_accessor :dacker, :dackerfile_path, :env 11 | 12 | def deploy!(filter=nil) 13 | filtered_containers(filter).each do |lf| 14 | ContainerDeployer.new( 15 | definition: lf[1] 16 | ).deploy! 17 | end if dacker 18 | end 19 | 20 | def filtered_containers(filter) 21 | if filter 22 | containers.select {|k| filter.include? k[0]} 23 | else 24 | containers 25 | end 26 | end 27 | 28 | def containers 29 | dacker.sort_by{|k,v| v["deploy"]["order"]} 30 | end 31 | 32 | def dackerfile 33 | @dackerfile ||= FileLoader.new( 34 | path: dackerfile_path, 35 | env: env 36 | ).content 37 | end 38 | 39 | def log(message, color=:green) 40 | Logger.log("#{message}", color) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/dacker/shell_runner.rb: -------------------------------------------------------------------------------- 1 | module Dacker 2 | class ShellRunner 3 | def initialize(options={}) 4 | @definition ||= options[:definition] 5 | end 6 | 7 | attr_accessor :definition 8 | 9 | def cmd(cmd) 10 | system "ssh #{username}@#{host} '#{docker_cmd} #{cmd}'" 11 | end 12 | 13 | def docker_cmd 14 | "sudo docker run -i -t #{volumes} #{environment} #{name}" 15 | end 16 | 17 | def volumes 18 | if definition["volumes"] 19 | definition["volumes"].collect do |v| 20 | " -v #{v}" 21 | end.join(" ") 22 | else 23 | "" 24 | end 25 | end 26 | 27 | def environment 28 | if definition["environment"] 29 | definition["environment"].collect do |v| 30 | " -e #{v}" 31 | end.join(" ") 32 | else 33 | "" 34 | end 35 | end 36 | 37 | def name 38 | definition["image"] || definition["deploy"]["name"] 39 | end 40 | 41 | def username 42 | definition["deploy"]["user"] 43 | end 44 | 45 | def host 46 | definition["deploy"]["host"] 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/dacker/version.rb: -------------------------------------------------------------------------------- 1 | module Dacker 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dacker/container_spec.rb: -------------------------------------------------------------------------------- 1 | require 'dacker' 2 | require 'spec_helper' 3 | require 'pry' 4 | require 'securerandom' 5 | 6 | describe Dacker::Container do 7 | let(:docker) { ::Docker::Connection.new('tcp://localhost:5000',{}) } 8 | let (:name) { @container.json["Name"][1..-1] } 9 | let (:container) do 10 | ::Dacker::Container.new( 11 | { 12 | config: {"image" => "ubuntu"}, 13 | name: name, 14 | host: '192.168.50.31', 15 | username: 'deploy' 16 | } 17 | ) 18 | end 19 | 20 | describe ".container" do 21 | 22 | before do 23 | @container = Docker::Container.create({'Cmd' => ["/bin/sh", '-c', 'while true; do echo Hello World; sleep 1; done'], 'Image' => 'ubuntu'}, docker) 24 | end 25 | 26 | context "when the container exists + not running" do 27 | it "should return it when running is false" do 28 | expect(container.container(false).id).to eq(@container.id) 29 | end 30 | 31 | it "should not return it when running is true" do 32 | expect(container.container(true)).to be_nil 33 | end 34 | end 35 | 36 | context "when the container exists + running" do 37 | before do 38 | @container.start 39 | end 40 | 41 | it "should return it when when running is true" do 42 | expect(container.container(true).id).to eq(@container.id) 43 | end 44 | end 45 | end 46 | 47 | describe ".signal" do 48 | before do 49 | @container = Docker::Container.create({'Cmd' => ["/bin/sh", '-c', 'while true; do echo Hello World; sleep 1; done'], 'Image' => 'ubuntu'}, docker) 50 | @container.start 51 | end 52 | 53 | it "-9 should stop the running container" do 54 | expect(container.container).to_not be_nil 55 | container.signal(9) 56 | expect(container.container).to be_nil 57 | end 58 | end 59 | 60 | describe ".start" do 61 | before do 62 | @container = Docker::Container.create({'Cmd' => ["/bin/sh", '-c', 'while true; do echo Hello World; sleep 1; done'], 'Image' => 'ubuntu'}, docker) 63 | end 64 | 65 | it "should start the container" do 66 | expect(container.container).to be_nil 67 | container.start 68 | expect(container.container).to_not be_nil 69 | end 70 | end 71 | 72 | describe ".build" do 73 | let (:container) do 74 | ::Dacker::Container.new( 75 | { 76 | config: {"build" => File.join(File.expand_path(File.dirname(File.dirname(__FILE__))),"support","docker_build_example") }, 77 | name: SecureRandom.hex, 78 | host: '192.168.50.31', 79 | username: 'deploy' 80 | } 81 | ) 82 | 83 | end 84 | 85 | before do 86 | container.build 87 | end 88 | 89 | it "should build the container" do 90 | expect(Docker::Image.exist?(container.image,{},docker)).to eq(true) 91 | end 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalkingQuickly/dacker/8555843f7f401a43fb4984102b04fb74c0a0c6cc/spec/spec_helper.rb -------------------------------------------------------------------------------- /spec/support/docker_build_example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:12.04 2 | -------------------------------------------------------------------------------- /templates/Vagrantfile: -------------------------------------------------------------------------------- 1 | $script = <