├── .gitignore ├── Dockerfile ├── Gemfile ├── README.md ├── Vagrantfile ├── build_and_test.sh ├── docker.conf └── spec ├── 192.168.50.4 ├── dockerfile_git_spec.rb └── dockerfile_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant/ 2 | .bundle/ 3 | vendor/ 4 | id_rsa.pub 5 | Gemfile.lock -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM base 2 | MAINTAINER tcnksm "https://github.com/tcnksm" 3 | 4 | # -------------------------------------------- 5 | # Setting for serverspec (ssh, sudoer) 6 | # -------------------------------------------- 7 | 8 | # Install ssh 9 | RUN apt-get update 10 | RUN apt-get install -y openssh-server 11 | 12 | # Setting ssh 13 | RUN mkdir /var/run/sshd 14 | RUN /usr/sbin/sshd 15 | EXPOSE 22 16 | 17 | # Creating user and setting its password 18 | RUN useradd taichi 19 | RUN echo taichi:pass | chpasswd 20 | 21 | # Setting ssh login without password (from OSX) 22 | RUN mkdir -p /home/taichi/.ssh 23 | RUN chown taichi /home/taichi/.ssh 24 | RUN chmod 700 /home/taichi/.ssh 25 | ADD ./id_rsa.pub /home/taichi/.ssh/authorized_keys 26 | RUN chown taichi /home/taichi/.ssh/authorized_keys 27 | RUN chmod 600 /home/taichi/.ssh/authorized_keys 28 | 29 | # Setting sudoer 30 | RUN echo "taichi ALL=(ALL) ALL" > /etc/sudoers.d/taichi 31 | 32 | 33 | # -------------------------------------------- 34 | # Build your own image 35 | # -------------------------------------------- 36 | 37 | # e.g., package installation tested by serverspec 38 | RUN apt-get -y install git 39 | 40 | # e.g., docker specific command tested by docker remote api 41 | WORKDIR /home/taichi 42 | ENV TEST test 43 | CMD ["sshd"] 44 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rspec' 4 | gem 'docker-api' 5 | gem 'serverspec' 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Test Driven Development of Dockerfile 2 | 3 | This is sample project for Test Driven Development (TDD) of Dockerfile by RSpec. This means developing dockerfile by below cycle, 4 | 5 | - Write Rspec test 6 | - Build Docker image and run test -> `RED` 7 | - Edit Dockerfile 8 | - Build Docker image and run test -> `GREEN` 9 | - Write Rs... 10 | 11 | When developing Dockerfile, docker image, we should test pakage installation and dockerfile specific command like `CMD` or `EXPOSE`. To do this I use belows, 12 | 13 | - To test package installation, [serverspec](https://github.com/serverspec/serverspec) 14 | - To test docker specific command, [docker-api](https://github.com/swipely/docker-api) which is [Docker Remote API]() wrapper. 15 | 16 | 17 | ## Environment 18 | 19 | - OSX 20 | - Docker on Vagrant VM 21 | 22 | ## Usage 23 | 24 | ``` 25 | $ ./build_and_test.sh 26 | ``` 27 | 28 | This script executes belows, 29 | 30 | 1. Build image, `docker -H :5422 build -t tcnksm/sample /vagrant/.` 31 | 1. Run container, `docker -H :5422 run -p 7654:22 -d serverspec /usr/sbin/sshd -D` 32 | 1. Run rspec test, `bundle exec rspec` 33 | 1. Delete container, `docker stop` and `docker rm` 34 | 35 | 36 | ## Vagrant 37 | 38 | In advance, run Vagrant VM 39 | 40 | ``` 41 | $ vagrant up 42 | ``` 43 | 44 | Assgin private network IP to Vagrant VM. And out ssh configuration, 45 | 46 | ``` 47 | $ vagrant ssh-config --host docker-vm >> ~/.ssh/config 48 | ``` 49 | 50 | ## serverspec 51 | 52 | ### Settings (ssh, sudoer) 53 | 54 | To use serverspec, we need to prepare docker image which is prepared ssh and sudo user, see `Dockerfile`. In `Dockerfile`, to login the docker container by ssh without password, use public key on your localhost, prepare `id_rsa.pub` in project directory. 55 | 56 | ### serverspec 57 | 58 | To install, 59 | 60 | ``` 61 | $ gem install serverspec 62 | ``` 63 | 64 | Initialize, 65 | 66 | ``` 67 | $ bundle serverspec-init 68 | Select OS type: 69 | 70 | 1) UN*X 71 | 2) Windows 72 | 73 | Select number: 1 74 | 75 | Select a backend type: 76 | 77 | 1) SSH 78 | 2) Exec (local) 79 | 80 | Select number: 1 81 | 82 | Vagrant instance y/n: n 83 | Input target host name: 192.168.50.4 84 | ``` 85 | 86 | In `Vagrantfile`, private network IP "192.168.50.4" is assined to Vagrant VM. You should use it for target host name. 87 | 88 | Edit `~/.ssh/config`, using vagrant private network IP. 89 | 90 | ``` 91 | Host 192.168.50.4 92 | HostName 192.168.50.4 93 | User taichi 94 | Port 7654 95 | UserKnownHostsFile /dev/null 96 | StrictHostKeyChecking no 97 | PasswordAuthentication no 98 | IdentityFile /Users/taichi/.ssh/id_rsa 99 | IdentitiesOnly yes 100 | LogLevel FATAL 101 | ``` 102 | 103 | Set environmental variable for sudo, 104 | 105 | ``` 106 | export SUDO_PASSWORD="pass" 107 | ``` 108 | 109 | or 110 | 111 | ``` 112 | export ASK_SUDO_PASSWORD=1 113 | ``` 114 | 115 | ### Writing Rspec test 116 | 117 | See sample `spec/192.168.50.4/dockerfile_git_spec.rb`. For more details, see [documents](http://serverspec.org/) 118 | 119 | 120 | ## Docker Remote API 121 | 122 | ### Settings 123 | 124 | By default, docker deamon listen on unix:///var/run/docker.sock. (see [here](http://docs.docker.io/en/latest/use/basics/#bind-docker)), To use it from external host, you should bind specific IP and port. To do this edit `/etc/init/docker.conf`. See `docker.conf` 125 | 126 | ### Docker Remote API 127 | 128 | Install, 129 | 130 | ``` 131 | $ gem install docker-api 132 | ``` 133 | 134 | Add below to `spec_helper.rb` 135 | 136 | ``` 137 | require "docker" 138 | Docker.url = "http://192.168.50.4:5422" 139 | ``` 140 | 141 | IP is private IP assined Vagrant VM, port is bind port of docker deamon set by `docker.conf` 142 | 143 | ### Writing Rspec test 144 | 145 | See sample `spec/192.168.50.4/dockerfile_spec.rb`. For more details, see [this blog post](http://blog.wercker.com/2013/12/23/Test-driven-development-for-docker.html). 146 | 147 | 148 | ## Reference 149 | 150 | - [serverspec - home](http://serverspec.org/) 151 | - [Experimenting with test driven development for docker](http://blog.wercker.com/2013/12/23/Test-driven-development-for-docker.html) 152 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "precise64" 6 | config.vm.box_url = "http://files.vagrantup.com/precise64.box" 7 | config.vm.network :private_network, ip: "192.168.50.4" 8 | config.vm.provision :docker do |d| 9 | d.pull_images "base" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /build_and_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Build docker image:" 4 | ssh docker-vm "cd /vagrant; docker -H :5422 build -t tcnksm/sample ." 5 | echo 6 | 7 | echo "Run container:" 8 | ssh docker-vm docker -H :5422 run -p 7654:22 -d tcnksm/sample /usr/sbin/sshd -D 9 | echo 10 | 11 | echo "Run rspec test:" 12 | bundle exec rspec 13 | echo 14 | 15 | echo "Delete container:" 16 | ssh docker-vm "docker -H :5422 stop `ssh docker-vm docker -H :5422 ps -l -q`" 17 | ssh docker-vm "docker -H :5422 rm `ssh docker-vm docker -H :5422 ps -l -q`" 18 | -------------------------------------------------------------------------------- /docker.conf: -------------------------------------------------------------------------------- 1 | description "Docker daemon" 2 | 3 | start on filesystem and started lxc-net 4 | stop on runlevel [!2345] 5 | 6 | respawn 7 | 8 | script 9 | DOCKER=/usr/bin/$UPSTART_JOB 10 | DOCKER_OPTS="-H 0.0.0.0:5422" 11 | if [ -f /etc/default/$UPSTART_JOB ]; then 12 | . /etc/default/$UPSTART_JOB 13 | fi 14 | "$DOCKER" -d $DOCKER_OPTS 15 | end script 16 | -------------------------------------------------------------------------------- /spec/192.168.50.4/dockerfile_git_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe package('git') do 4 | it { should be_installed } 5 | end 6 | -------------------------------------------------------------------------------- /spec/192.168.50.4/dockerfile_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Sample Images" do 4 | 5 | before(:all) do 6 | @image = Docker::Image.all.detect{|image| image.info["Repository"] == "tcnksm/sample"} 7 | end 8 | 9 | it "should exist" do 10 | expect(@image).not_to be_nil 11 | end 12 | 13 | it "should expose the default port" do 14 | expect(@image.json["config"]["ExposedPorts"].has_key?("22/tcp")).to be_true 15 | end 16 | 17 | it "should have CMD" do 18 | expect(@image.json["config"]["Cmd"]).to include("sshd") 19 | end 20 | 21 | it "should have working directory" do 22 | expect(@image.json["config"]["WorkingDir"]).to eq("/home/taichi") 23 | end 24 | 25 | it "should have environmental variable" do 26 | expect(@image.json["config"]["Env"]).to include("TEST=test") 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'serverspec' 2 | require 'pathname' 3 | require 'net/ssh' 4 | require 'docker' 5 | 6 | # For docker-api 7 | Docker.url = "http://192.168.50.4:5422" 8 | 9 | # For serverspec 10 | include SpecInfra::Helper::Ssh 11 | include SpecInfra::Helper::DetectOS 12 | 13 | RSpec.configure do |c| 14 | if ENV['ASK_SUDO_PASSWORD'] 15 | require 'highline/import' 16 | c.sudo_password = ask("Enter sudo password: ") { |q| q.echo = false } 17 | else 18 | c.sudo_password = ENV['SUDO_PASSWORD'] 19 | end 20 | c.before :all do 21 | block = self.class.metadata[:example_group_block] 22 | if RUBY_VERSION.start_with?('1.8') 23 | file = block.to_s.match(/.*@(.*):[0-9]+>/)[1] 24 | else 25 | file = block.source_location.first 26 | end 27 | host = File.basename(Pathname.new(file).dirname) 28 | if c.host != host 29 | c.ssh.close if c.ssh 30 | c.host = host 31 | options = Net::SSH::Config.for(c.host) 32 | user = options[:user] || Etc.getlogin 33 | c.ssh = Net::SSH.start(host, user, options) 34 | end 35 | end 36 | end 37 | --------------------------------------------------------------------------------