├── lib ├── beaker-docker.rb ├── beaker-docker │ └── version.rb └── beaker │ └── hypervisor │ ├── container.rb │ ├── container_docker.rb │ ├── container_podman.rb │ ├── container_swarm.rb │ └── docker.rb ├── .editorconfig ├── acceptance ├── tests │ └── 00_default_spec.rb └── config │ └── nodes │ └── hosts.yaml ├── Gemfile ├── .rubocop.yml ├── .gitignore ├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── release.yml │ └── test.yml ├── spec ├── spec_helper.rb └── beaker │ └── hypervisor │ └── docker_spec.rb ├── .rubocop_todo.yml ├── beaker-docker.gemspec ├── bin └── beaker-docker ├── Rakefile ├── README.md ├── docker.md ├── LICENSE └── CHANGELOG.md /lib/beaker-docker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | -------------------------------------------------------------------------------- /lib/beaker-docker/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BeakerDocker 4 | VERSION = '3.0.1' 5 | end 6 | -------------------------------------------------------------------------------- /lib/beaker/hypervisor/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'beaker/hypervisor/docker' 4 | 5 | module Beaker 6 | class Container < Beaker::Docker 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/beaker/hypervisor/container_docker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'beaker/hypervisor/container' 4 | 5 | module Beaker 6 | class ContainerDocker < Beaker::Container 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/beaker/hypervisor/container_podman.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'beaker/hypervisor/container' 4 | 5 | module Beaker 6 | class ContainerPodman < Beaker::Container 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/beaker/hypervisor/container_swarm.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'beaker/hypervisor/container' 4 | 5 | module Beaker 6 | class ContainerSwarm < Beaker::Container 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | tab_width = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /acceptance/tests/00_default_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'beaker' 4 | 5 | test_name 'Ensure docker container is accessible' do 6 | hosts.each do |host| 7 | step "on #{host}" do 8 | on(host, 'true') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source ENV['GEM_SOURCE'] || 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :release, optional: true do 8 | gem 'faraday-retry', '~> 2.1', require: false 9 | gem 'github_changelog_generator', '~> 1.16.4', require: false 10 | end 11 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit_from: .rubocop_todo.yml 3 | 4 | inherit_gem: 5 | voxpupuli-rubocop: rubocop.yml 6 | 7 | Naming/FileName: 8 | Description: Some files violates the snake_case convention 9 | Exclude: 10 | - 'lib/beaker-docker.rb' 11 | 12 | AllCops: 13 | TargetRubyVersion: 3.2 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | log/* 3 | !.gitignore 4 | junit 5 | acceptance-tests 6 | pkg 7 | Gemfile.lock 8 | options.rb 9 | test.cfg 10 | .yardoc 11 | coverage 12 | .bundle 13 | .vendor 14 | _vendor 15 | vendor 16 | tmp/ 17 | doc 18 | # JetBrains IDEA 19 | *.iml 20 | .idea/ 21 | # rbenv file 22 | .ruby-version 23 | .ruby-gemset 24 | # Vagrant folder 25 | .vagrant/ 26 | .vagrant_files/ 27 | vendor/bundle 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # raise PRs for gem updates 4 | - package-ecosystem: bundler 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: "13:00" 9 | open-pull-requests-limit: 10 10 | 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | time: "13:00" 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'beaker' 4 | 5 | Dir['./lib/beaker/hypervisor/*.rb'].each { |file| require file } 6 | 7 | # setup & require beaker's spec_helper.rb 8 | beaker_gem_spec = Gem::Specification.find_by_name('beaker') 9 | beaker_gem_dir = beaker_gem_spec.gem_dir 10 | beaker_spec_path = File.join(beaker_gem_dir, 'spec') 11 | $LOAD_PATH << beaker_spec_path 12 | require File.join(beaker_spec_path, 'spec_helper.rb') 13 | 14 | RSpec.configure do |config| 15 | config.include TestFileHelpers 16 | config.include HostHelpers 17 | end 18 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 3 | 4 | changelog: 5 | exclude: 6 | labels: 7 | - duplicate 8 | - invalid 9 | - modulesync 10 | - question 11 | - skip-changelog 12 | - wont-fix 13 | - wontfix 14 | - github_actions 15 | 16 | categories: 17 | - title: Breaking Changes 🛠 18 | labels: 19 | - backwards-incompatible 20 | 21 | - title: New Features 🎉 22 | labels: 23 | - enhancement 24 | 25 | - title: Bug Fixes 🐛 26 | labels: 27 | - bug 28 | - bugfix 29 | 30 | - title: Documentation Updates 📚 31 | labels: 32 | - documentation 33 | - docs 34 | 35 | - title: Dependency Updates ⬆️ 36 | labels: 37 | - dependencies 38 | 39 | - title: Other Changes 40 | labels: 41 | - "*" 42 | -------------------------------------------------------------------------------- /acceptance/config/nodes/hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | HOSTS: 3 | centos9: 4 | platform: el-9-x86_64 5 | hypervisor: docker 6 | image: quay.io/centos/centos:stream9 7 | roles: 8 | - master 9 | - agent 10 | - dashboard 11 | - database 12 | - classifier 13 | - default 14 | docker_cmd: '["/sbin/init"]' 15 | debian12: 16 | platform: debian-12-x86_64 17 | hypervisor: docker 18 | image: debian:12 19 | roles: 20 | - agent 21 | docker_cmd: '["/sbin/init"]' 22 | CONFIG: 23 | nfs_server: none 24 | consoleport: 443 25 | log_level: verbose 26 | # Ubuntu runners need to run with full privileges 27 | # RHEL derivitives just need the docker cap AUDIT_WRITE 28 | dockeropts: 29 | HostConfig: 30 | Privileged: true 31 | # docker_cap_add: 32 | # - AUDIT_WRITE 33 | type: aio 34 | ssh: 35 | verify_host_key: false 36 | user_known_hosts_file: '/dev/null' 37 | password: root 38 | auth_methods: 39 | - password 40 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config --no-auto-gen-timestamp` 3 | # using RuboCop version 1.79.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 39 10 | # Configuration parameters: CountAsOne. 11 | RSpec/ExampleLength: 12 | Max: 36 13 | 14 | # Offense count: 60 15 | # Configuration parameters: . 16 | # SupportedStyles: have_received, receive 17 | RSpec/MessageSpies: 18 | EnforcedStyle: receive 19 | 20 | # Offense count: 17 21 | # Configuration parameters: AllowSubject. 22 | RSpec/MultipleMemoizedHelpers: 23 | Max: 12 24 | 25 | # Offense count: 7 26 | # This cop supports unsafe autocorrection (--autocorrect-all). 27 | RSpec/ReceiveMessages: 28 | Exclude: 29 | - 'spec/beaker/hypervisor/docker_spec.rb' 30 | 31 | # Offense count: 19 32 | # This cop supports safe autocorrection (--autocorrect). 33 | # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. 34 | # URISchemes: http, https 35 | Layout/LineLength: 36 | Max: 198 37 | -------------------------------------------------------------------------------- /beaker-docker.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('lib', __dir__) 4 | require 'beaker-docker/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'beaker-docker' 8 | s.version = BeakerDocker::VERSION 9 | s.authors = [ 10 | 'Vox Pupuli', 11 | 'Rishi Javia', 12 | 'Kevin Imber', 13 | 'Tony Vu', 14 | ] 15 | s.email = ['voxpupuli@groups.io'] 16 | s.homepage = 'https://github.com/voxpupuli/beaker-docker' 17 | s.summary = 'Docker hypervisor for Beaker acceptance testing framework' 18 | s.description = 'Allows running Beaker tests using Docker' 19 | s.license = 'Apache-2.0' 20 | 21 | s.files = `git ls-files`.split("\n") 22 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 23 | s.require_paths = ['lib'] 24 | 25 | s.required_ruby_version = '>= 3.2', '< 4' 26 | 27 | # Testing dependencies 28 | s.add_development_dependency 'fakefs', '>= 1.3', '< 4' 29 | s.add_development_dependency 'rake', '~> 13.0' 30 | s.add_development_dependency 'rspec', '~> 3.0' 31 | s.add_development_dependency 'voxpupuli-rubocop', '~> 4.2.0' 32 | 33 | # Run time dependencies 34 | s.add_dependency 'beaker', '>= 4', '< 8' 35 | s.add_dependency 'docker-api', '~> 2.3' 36 | # excon is a docker-api dependency, 1.2.6 is broken 37 | # https://github.com/excon/excon/issues/884 38 | s.add_dependency 'excon', '>= 1.2.5', '< 2', '!= 1.2.6' 39 | s.add_dependency 'stringify-hash', '~> 0.0.0' 40 | end 41 | -------------------------------------------------------------------------------- /bin/beaker-docker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'rubygems' unless defined?(Gem) 5 | require 'beaker' 6 | require 'beaker-docker' 7 | 8 | def dockerfile(hostspec, filename) 9 | ENV['BEAKER_HYPERVISOR'] = 'docker' 10 | options = Beaker::Options::Parser.new.parse_args(['--hosts', hostspec || '', '--no-provision']) 11 | options[:logger] = Beaker::Logger.new(options) 12 | network_manager = Beaker::NetworkManager.new(options, options[:logger]) 13 | network_manager.provision 14 | hosts = network_manager.hosts 15 | 16 | if hosts.size != 1 17 | options[:logger].error "Found #{hosts.size} hosts, expected 1" 18 | exit(1) 19 | end 20 | 21 | hypervisor = network_manager.hypervisors['docker'] 22 | # TODO: private method 23 | File.write(filename, hypervisor.send(:dockerfile_for, hosts.first)) 24 | end 25 | 26 | VERSION_STRING = <<'VER' 27 | _ .--. 28 | ( ` ) 29 | beaker-docker .-' `--, 30 | _..----.. ( )`-. 31 | .'_|` _|` _|( .__, ) 32 | /_| _| _| _( (_, .-' 33 | ;| _| _| _| '-'__,--'`--' 34 | | _| _| _| _| | 35 | _ || _| _| _| _| %s 36 | _( `--.\_| _| _| _|/ 37 | .-' )--,| _| _|.` 38 | (__, (_ ) )_| _| / 39 | `-.__.\ _,--'\|__|__/ 40 | ;____; 41 | \YT/ 42 | || 43 | |""| 44 | '==' 45 | VER 46 | 47 | case ARGV[0] 48 | when 'containerfile' 49 | dockerfile(ARGV[1], ARGV[2] || 'Containerfile') 50 | when 'dockerfile' 51 | dockerfile(ARGV[1], ARGV[2] || 'Dockerfile') 52 | else 53 | puts VERSION_STRING % BeakerDocker::VERSION 54 | end 55 | 56 | exit 0 57 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/core/rake_task' 4 | 5 | begin 6 | require 'voxpupuli/rubocop/rake' 7 | rescue LoadError 8 | # the voxpupuli-rubocop gem is optional 9 | end 10 | 11 | namespace :test do 12 | namespace :spec do 13 | desc 'Run spec tests' 14 | RSpec::Core::RakeTask.new(:run) do |t| 15 | t.rspec_opts = ['--color', '--format documentation'] 16 | t.pattern = 'spec/' 17 | end 18 | end 19 | 20 | namespace :acceptance do 21 | desc 'A quick acceptance test, named because it has no pre-suites to run' 22 | task :quick do 23 | ## setup & load_path of beaker's acceptance base and lib directory 24 | ## see below for the reason why it's commented out atm 25 | # beaker_gem_spec = Gem::Specification.find_by_name('beaker') 26 | # beaker_gem_dir = beaker_gem_spec.gem_dir 27 | # beaker_test_base_dir = File.join(beaker_gem_dir, 'acceptance/tests/base') 28 | # load_path_option = File.join(beaker_gem_dir, 'acceptance/lib') 29 | 30 | ENV['BEAKER_setfile'] = 'acceptance/config/nodes/hosts.yaml' 31 | sh('beaker', 32 | '--hosts', 'acceptance/config/nodes/hosts.yaml', 33 | ## We can't run these tests until the rsync support in the main 34 | ## beaker/host.rb is updated to work with passwords. 35 | # '--tests', beaker_test_base_dir, 36 | # '--load-path', load_path_option, 37 | '--tests', 'acceptance/tests/', 38 | '--log-level', 'debug', 39 | '--debug') 40 | end 41 | end 42 | end 43 | 44 | # namespace-named default tasks. 45 | # these are the default tasks invoked when only the namespace is referenced. 46 | # they're needed because `task :default` in those blocks doesn't work as expected. 47 | task 'test:spec': %i[test:spec:run] 48 | task 'test:acceptance': %i[test:acceptance:quick] 49 | 50 | # global defaults 51 | task lint: %i[lint:rubocop] 52 | task test: %i[test:spec] 53 | task default: %i[test] 54 | 55 | begin 56 | require 'rubygems' 57 | require 'github_changelog_generator/task' 58 | rescue LoadError 59 | # Do nothing if no required gem installed 60 | else 61 | GitHubChangelogGenerator::RakeTask.new :changelog do |config| 62 | config.exclude_labels = %w[duplicate question invalid wontfix wont-fix skip-changelog github_actions] 63 | config.user = 'voxpupuli' 64 | config.project = 'beaker-docker' 65 | gem_version = Gem::Specification.load("#{config.project}.gemspec").version 66 | config.future_release = gem_version 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Gem Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | build-release: 13 | # Prevent releases from forked repositories 14 | if: github.repository_owner == 'voxpupuli' 15 | name: Build the gem 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v6 19 | - name: Install Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: 'ruby' 23 | - name: Build gem 24 | shell: bash 25 | run: gem build --verbose *.gemspec 26 | - name: Upload gem to GitHub cache 27 | uses: actions/upload-artifact@v6 28 | with: 29 | name: gem-artifact 30 | path: '*.gem' 31 | retention-days: 1 32 | compression-level: 0 33 | 34 | create-github-release: 35 | needs: build-release 36 | name: Create GitHub release 37 | runs-on: ubuntu-24.04 38 | permissions: 39 | contents: write # clone repo and create release 40 | steps: 41 | - name: Download gem from GitHub cache 42 | uses: actions/download-artifact@v7 43 | with: 44 | name: gem-artifact 45 | - name: Create Release 46 | shell: bash 47 | env: 48 | GH_TOKEN: ${{ github.token }} 49 | run: gh release create --repo ${{ github.repository }} ${{ github.ref_name }} --generate-notes *.gem 50 | 51 | release-to-github: 52 | needs: build-release 53 | name: Release to GitHub 54 | runs-on: ubuntu-24.04 55 | permissions: 56 | packages: write # publish to rubygems.pkg.github.com 57 | steps: 58 | - name: Download gem from GitHub cache 59 | uses: actions/download-artifact@v7 60 | with: 61 | name: gem-artifact 62 | - name: Publish gem to GitHub packages 63 | run: gem push --host https://rubygems.pkg.github.com/${{ github.repository_owner }} *.gem 64 | env: 65 | GEM_HOST_API_KEY: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | release-to-rubygems: 68 | needs: build-release 69 | name: Release gem to rubygems.org 70 | runs-on: ubuntu-24.04 71 | environment: release # recommended by rubygems.org 72 | permissions: 73 | id-token: write # rubygems.org authentication 74 | steps: 75 | - name: Download gem from GitHub cache 76 | uses: actions/download-artifact@v7 77 | with: 78 | name: gem-artifact 79 | - uses: rubygems/configure-rubygems-credentials@v1.0.0 80 | - name: Publish gem to rubygems.org 81 | shell: bash 82 | run: gem push *.gem 83 | 84 | release-verification: 85 | name: Check that all releases are done 86 | runs-on: ubuntu-24.04 87 | permissions: 88 | contents: read # minimal permissions that we have to grant 89 | needs: 90 | - create-github-release 91 | - release-to-github 92 | - release-to-rubygems 93 | steps: 94 | - name: Download gem from GitHub cache 95 | uses: actions/download-artifact@v7 96 | with: 97 | name: gem-artifact 98 | - name: Install Ruby 99 | uses: ruby/setup-ruby@v1 100 | with: 101 | ruby-version: 'ruby' 102 | - name: Wait for release to propagate 103 | shell: bash 104 | run: | 105 | gem install rubygems-await 106 | gem await *.gem 107 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | 4 | on: 5 | pull_request: {} 6 | push: 7 | branches: 8 | - master 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | rubocop_and_matrix: 15 | runs-on: ubuntu-24.04 16 | outputs: 17 | ruby: ${{ steps.ruby.outputs.versions }} 18 | steps: 19 | - uses: actions/checkout@v6 20 | - name: Install Ruby 3.4 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: "3.4" 24 | bundler-cache: true 25 | - name: Run Rubocop 26 | run: bundle exec rake rubocop 27 | - id: ruby 28 | uses: voxpupuli/ruby-version@v1 29 | 30 | rspec: 31 | runs-on: ubuntu-24.04 32 | needs: rubocop_and_matrix 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | ruby: ${{ fromJSON(needs.rubocop_and_matrix.outputs.ruby) }} 37 | name: RSpec - Ruby ${{ matrix.ruby }} 38 | steps: 39 | - uses: actions/checkout@v6 40 | - name: Install Ruby ${{ matrix.ruby }} 41 | uses: ruby/setup-ruby@v1 42 | with: 43 | ruby-version: ${{ matrix.ruby }} 44 | bundler-cache: true 45 | - name: spec tests 46 | run: bundle exec rake test:spec 47 | - name: Build gem 48 | run: gem build --strict --verbose *.gemspec 49 | 50 | docker: 51 | runs-on: ubuntu-24.04 52 | needs: rubocop_and_matrix 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | ruby: ${{ fromJSON(needs.rubocop_and_matrix.outputs.ruby) }} 57 | name: Docker on Ruby ${{ matrix.ruby }} 58 | steps: 59 | - uses: actions/checkout@v6 60 | - name: Install Ruby ${{ matrix.ruby }} 61 | uses: ruby/setup-ruby@v1 62 | with: 63 | ruby-version: ${{ matrix.ruby }} 64 | bundler-cache: true 65 | - name: Run acceptance tests 66 | run: bundle exec rake test:acceptance 67 | 68 | # this job is currently broken because puppet/puppet-dev-tools:2023-02-24-1bca42e is too old 69 | #beaker_in_container: 70 | # runs-on: ubuntu-24.04 71 | # needs: rubocop_and_matrix 72 | # name: Docker - Beaker in container connection test 73 | # steps: 74 | # - uses: actions/checkout@v6 75 | # # use this and not container key from gha to not have a docker network from github 76 | # - name: Run Beaker in docker container 77 | # uses: addnab/docker-run-action@v3 78 | # with: 79 | # image: puppet/puppet-dev-tools:2023-02-24-1bca42e 80 | # options: -v ${{ github.workspace }}:/work 81 | # run: | 82 | # cd /work 83 | # ls -la 84 | # bundle install 85 | # export DOCKER_IN_DOCKER=true 86 | # bundle exec rake test:acceptance 87 | 88 | # verifies that podman service is a dropin replacement for docker 89 | podman: 90 | runs-on: ubuntu-24.04 91 | needs: rubocop_and_matrix 92 | strategy: 93 | fail-fast: false 94 | matrix: 95 | ruby: ${{ fromJSON(needs.rubocop_and_matrix.outputs.ruby) }} 96 | name: Podman on Ruby ${{ matrix.ruby }} 97 | steps: 98 | - name: Start podman 99 | run: | 100 | sudo systemctl stop docker.service docker.socket && systemctl start --user podman.socket 101 | echo "DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock" >> "$GITHUB_ENV" 102 | - uses: actions/checkout@v6 103 | - name: Install Ruby ${{ matrix.ruby }} 104 | uses: ruby/setup-ruby@v1 105 | with: 106 | ruby-version: ${{ matrix.ruby }} 107 | bundler-cache: true 108 | - name: Run acceptance tests 109 | run: bundle exec rake test:acceptance 110 | 111 | tests: 112 | needs: 113 | - rubocop_and_matrix 114 | - docker 115 | # - beaker_in_container 116 | - podman 117 | - rspec 118 | runs-on: ubuntu-24.04 119 | name: Test suite 120 | steps: 121 | - run: echo Test suite completed 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # beaker-docker 2 | 3 | [![License](https://img.shields.io/github/license/voxpupuli/beaker-docker.svg)](https://github.com/voxpupuli/beaker-docker/blob/master/LICENSE) 4 | [![Test](https://github.com/voxpupuli/beaker-docker/actions/workflows/test.yml/badge.svg)](https://github.com/voxpupuli/beaker-docker/actions/workflows/test.yml) 5 | [![codecov](https://codecov.io/gh/voxpupuli/beaker-docker/branch/master/graph/badge.svg?token=Mypkl78hvK)](https://codecov.io/gh/voxpupuli/beaker-docker) 6 | [![Release](https://github.com/voxpupuli/beaker-docker/actions/workflows/release.yml/badge.svg)](https://github.com/voxpupuli/beaker-docker/actions/workflows/release.yml) 7 | [![RubyGem Version](https://img.shields.io/gem/v/beaker-docker.svg)](https://rubygems.org/gems/beaker-docker) 8 | [![RubyGem Downloads](https://img.shields.io/gem/dt/beaker-docker.svg)](https://rubygems.org/gems/beaker-docker) 9 | [![Donated by Puppet Inc](https://img.shields.io/badge/donated%20by-Puppet%20Inc-fb7047.svg)](#transfer-notice) 10 | 11 | Beaker library to use docker hypervisor 12 | 13 | * [How to use this wizardry](#how-to-use-this-wizardry) 14 | * [Nodeset Options](#nodeset-options) 15 | * [Privileged containers](#privileged-containers) 16 | * [Cleaning up after tests](#cleaning-up-after-tests) 17 | * [Working with `podman`](#working-with-podman) 18 | * [Generating a Dockerfile](#generating-a-dockerfile) 19 | * [Spec tests]() 20 | * [Acceptance tests]() 21 | * [Transfer Notice](#transfer-notice) 22 | * [License](#license) 23 | * [Release Information](#release-information) 24 | 25 | ## How to use this wizardry 26 | 27 | This gem that allows you to use hosts with [docker](docker.md) hypervisor with [beaker](https://github.com/voxpupuli/beaker). 28 | 29 | Beaker will automatically load the appropriate hypervisors for any given hosts 30 | file, so as long as your project dependencies are satisfied there's nothing else 31 | to do. No need to `require` this library in your tests. 32 | 33 | In order to use a specific hypervisor or DSL extension library in your project, 34 | you will need to include them alongside Beaker in your Gemfile or 35 | project.gemspec. E.g. 36 | 37 | ```ruby 38 | # Gemfile 39 | gem 'beaker', '~> 4.0' 40 | gem 'beaker-docker' 41 | # project.gemspec 42 | s.add_runtime_dependency 'beaker', '~> 4.0' 43 | s.add_runtime_dependency 'beaker-docker' 44 | ``` 45 | 46 | ### Nodeset Options 47 | 48 | The following is a sample nodeset: 49 | 50 | ```yaml 51 | HOSTS: 52 | el8: 53 | platform: el-8-x86_64 54 | hypervisor: docker 55 | image: centos:8 56 | docker_cmd: '["/sbin/init"]' 57 | # Run arbitrary things 58 | docker_image_commands: 59 | - 'touch /tmp/myfile' 60 | dockeropts: 61 | Labels: 62 | thing: 'stuff' 63 | HostConfig: 64 | Privileged: true 65 | el7: 66 | platform: el-7-x86_64 67 | hypervisor: docker 68 | image: centos:7 69 | # EL7 images do not support nested systemd 70 | docker_cmd: '/usr/sbin/sshd -D -E /var/log/sshd.log' 71 | CONFIG: 72 | docker_cap_add: 73 | - AUDIT_WRITE 74 | ``` 75 | 76 | ### Privileged containers 77 | 78 | Containers are run in privileged mode by default unless capabilities are set. 79 | 80 | If you wish to disable privileged mode, simply set the following in your node: 81 | 82 | ```yaml 83 | dockeropts: 84 | HostConfig: 85 | Privileged: false 86 | ``` 87 | 88 | ### Cleaning up after tests 89 | 90 | Containers created by this plugin may not be destroyed unless the tests complete 91 | successfully. Each container created is prefixed by `beaker-` to make filtering 92 | for clean up easier. 93 | 94 | A quick way to clean up all nodes is as follows: 95 | 96 | ```sh 97 | podman rm -f $( podman ps -q -f name="beaker-*" ) 98 | ``` 99 | 100 | ## Working with `podman` 101 | 102 | If you're using a version of `podman` that has API socket support then you 103 | should be able to simply set `DOCKER_HOST` to your socket and connect as usual. 104 | 105 | You also need to ensure that you're using a version of the `docker-api` gem that 106 | supports `podman`. 107 | 108 | You may find that not all of your tests work as expected. This will be due to 109 | the tighter system restrictions placed on containers by `podman`. You may need 110 | to edit the `dockeropts` hash in your nodeset to include different flags in the 111 | `HostConfig` section. 112 | 113 | See the 114 | [HostConfig](https://any-api.com/docker_com/engine/docs/Definitions/HostConfig) 115 | portion of the docker API for more information. 116 | 117 | ## Generating a Dockerfile 118 | 119 | Usually beaker-docker is used to provision docker instances with beaker. During 120 | this step beaker-docker generates a Dockerfile and posts it to the docker daemon 121 | API. 122 | 123 | There's also a small CLI command to only generate the file: 124 | 125 | ``` 126 | bundle exec beaker-docker dockerfile archlinux-64 127 | ``` 128 | 129 | Will generate a local `Dockerfile`: 130 | 131 | ```dockerfile 132 | FROM archlinux/archlinux 133 | ENV container docker 134 | RUN pacman --sync --refresh --noconfirm archlinux-keyring && pacman --sync --refresh --noconfirm --sysupgrade && pacman --sync --noconfirm curl ntp net-tools openssh && ssh-keygen -A && sed -ri 's/^#?UsePAM .*/UsePAM no/' /etc/ssh/sshd_config && systemctl enable sshd 135 | RUN mkdir -p /var/run/sshd && echo root:root | chpasswd 136 | RUN sed -ri -e 's/^#?PermitRootLogin .*/PermitRootLogin yes/' -e 's/^#?PasswordAuthentication .*/PasswordAuthentication yes/' -e 's/^#?UseDNS .*/UseDNS no/' -e 's/^#?MaxAuthTries.*/MaxAuthTries 1000/' /etc/ssh/sshd_config 137 | EXPOSE 22 138 | CMD ["/sbin/init"] 139 | ``` 140 | 141 | This works by calling 142 | (beaker-hostgenerator](https://github.com/voxpupuli/beaker-hostgenerator?tab=readme-ov-file#beaker-host-generator). 143 | So you can provide any host string that's supported by beaker-hostgenerator. 144 | 145 | For non-rolling release distros this is usually `$os$majorversion-$architecture` 146 | 147 | ``` 148 | beaker-docker dockerfile centos9-64 149 | ``` 150 | 151 | ```dockerfile 152 | FROM quay.io/centos/centos:stream9 153 | ENV container docker 154 | RUN dnf clean all && dnf install -y sudo openssh-server openssh-clients chrony && ssh-keygen -A && sed 's@session *required *pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/* 155 | RUN mkdir -p /var/run/sshd && echo root:root | chpasswd 156 | RUN sed -ri -e 's/^#?PermitRootLogin .*/PermitRootLogin yes/' -e 's/^#?PasswordAuthentication .*/PasswordAuthentication yes/' -e 's/^#?UseDNS .*/UseDNS no/' -e 's/^#?MaxAuthTries.*/MaxAuthTries 1000/' /etc/ssh/sshd_config 157 | RUN cp /bin/true /sbin/agetty 158 | RUN dnf install -y cronie crontabs initscripts iproute openssl wget which glibc-langpack-en hostname 159 | EXPOSE 22 160 | CMD ["/sbin/init"] 161 | ``` 162 | 163 | This requires a running docker daemon. You can also request a containerfile. 164 | This will currently generate a `Containerfile` but with the same content (this 165 | may change in the future, depending on the API spec). 166 | 167 | ## Spec tests 168 | 169 | Spec test live under the `spec` folder. There are the default rake task and therefore can run with a simple command: 170 | 171 | ```bash 172 | bundle exec rake test:spec 173 | ``` 174 | 175 | ## Acceptance tests 176 | 177 | There is a simple rake task to invoke acceptance test for the library: 178 | 179 | ```bash 180 | bundle exec rake test:acceptance 181 | ``` 182 | 183 | ## Transfer Notice 184 | 185 | This plugin was originally authored by [Puppet Inc](http://puppet.com). 186 | The maintainer preferred that Puppet Community take ownership of the module for future improvement and maintenance. 187 | Existing pull requests and issues were transferred over, please fork and continue to contribute here. 188 | 189 | Previously: https://github.com/puppetlabs/beaker 190 | 191 | ## License 192 | 193 | This gem is licensed under the Apache-2 license. 194 | 195 | ## Release information 196 | 197 | To make a new release, please do: 198 | * update the version in `lib/beaker-docker/version.rb` 199 | * Install gems with `bundle install --with release --path .vendor` 200 | * generate the changelog with `bundle exec rake changelog` 201 | * Check if the new version matches the closed issues/PRs in the changelog 202 | * Create a PR with it 203 | * After it got merged, push a tag. GitHub actions will do the actual release to rubygems and GitHub Packages 204 | -------------------------------------------------------------------------------- /docker.md: -------------------------------------------------------------------------------- 1 | This option allows for testing against Docker containers. 2 | 3 | 4 | ### Why? 5 | 6 | Using docker as a hypervisor significantly speeds up the provisioning process, as you don't have to spin up an entire VM to run the tests, which has significant overhead. 7 | 8 | ### How? 9 | 10 | So first of all, install Docker using the instructions [here](https://docs.docker.com/installation/#installation). 11 | 12 | In the real world, it's generally seen as [bad practice to have sshd running in a Docker container](http://blog.docker.com/2014/06/why-you-dont-need-to-run-sshd-in-docker/). However, for the purpose of a disposable test instance, we're not going to worry about that! 13 | 14 | ### Basic docker hosts file ### 15 | The base image to use for the container is named by the image key. 16 | 17 | HOSTS: 18 | ubuntu-12-10: 19 | platform: ubuntu-12.10-x64 20 | image: ubuntu:12.10 21 | hypervisor: docker 22 | CONFIG: 23 | type: foss 24 | 25 | ### Docker hosts file, with image modification ### 26 | You can specify extra commands to be executed in order to modify the image with the keys `docker_image_commands` and 27 | `docker_image_first_commands`. 28 | 29 | `docker_image_commands` is executed after initial setup. `docker_image_first_commands` is executed before any other 30 | commands and can be used eg. to configure a proxy. 31 | 32 | HOSTS: 33 | ubuntu-12-10: 34 | platform: ubuntu-12.10-x64 35 | image: ubuntu:12.10 36 | hypervisor: docker 37 | docker_image_first_commands: 38 | - echo 'Acquire::http::Proxy "http://proxy.example.com:3128";'> /etc/apt/apt.conf.d/01proxy 39 | - echo "export http_proxy=http://proxy.example.com:3128"> /etc/profile.d/proxy.sh 40 | - echo "export https_proxy=http://proxy.example.com:3128">> /etc/profile.d/proxy.sh 41 | - echo "export no_proxy=127.0.0.1,::1">> /etc/profile.d/proxy.sh 42 | docker_image_commands: 43 | - 'apt-get install -y myapp' 44 | - 'myapp --setup' 45 | CONFIG: 46 | type: foss 47 | 48 | ### Docker hosts files, with modified start commands ### 49 | By default the docker container just runs an sshd which is adequate for 'puppet apply' style testing. You can specify a different command to start with the `docker_cmd` key. This gives you scope to run something with more service supervision baked into it, but it is is important that this command starts an sshd listening on port 22 so that beaker can drive the container. 50 | 51 | HOSTS: 52 | ubuntu-12-10: 53 | platform: ubuntu-12.10-x64 54 | image: ubuntu:12.10 55 | hypervisor: docker 56 | docker_cmd: '["/sbin/init"]' 57 | CONFIG: 58 | type: foss 59 | 60 | ### Using the entrypoint of an image and not sshd ### 61 | Instead of using ssh as the CMD for a container, beaker will use the entrypoint already defined if `use_image_entry_point` is used. Beaker will still load ssh onto the container and start it, but ssh will not be the entrypoint for the container. Below is an example of using the puppetserver image. 62 | 63 | HOSTS: 64 | puppetserver: 65 | platform: ubuntu-1604-x86_64 66 | hypervisor: docker 67 | image: puppet/puppetserver-standalone:6.0.1 68 | use_image_entry_point: true 69 | roles: 70 | - master 71 | CONFIG: 72 | type: foss 73 | 74 | ### Using dockerfiles with beaker hosts files ### 75 | Beaker can utilize a dockerfile specified in hosts file; use the `dockerfile` attribute of a host to specify the location of the dockerfile. Beaker will use the directory it is run in to pass as the context for dockerfile DSL commands such as COPY and VOLUME, so make sure the paths are set correctly for the right context. 76 | 77 | HOSTS: 78 | ubuntu-12-10: 79 | platform: ubuntu-12.10-x64 80 | dockerfile: path/to/my/dockerfile 81 | hypervisor: docker 82 | docker_cmd: '["/sbin/init"]' 83 | CONFIG: 84 | type: foss 85 | 86 | ### Preserve Docker Image ### 87 | Unless the image configuration changes you might want to keep the Docker image for multiple spec runs. Use `docker_preserve_image` option for a host. 88 | 89 | HOSTS: 90 | ubuntu-12-10: 91 | platform: ubuntu-12.10-x64 92 | image: ubuntu:12.10 93 | hypervisor: docker 94 | docker_preserve_image: true 95 | CONFIG: 96 | type: foss 97 | 98 | ### Tag a built Docker Image ### 99 | Tag an image after creation; this allows for subsequent hosts to reference that image for multi-stage builds. 100 | 101 | HOSTS: 102 | ubuntu-12-10: 103 | platform: ubuntu-12.10-x64 104 | dockerfile: path/to/file 105 | hypervisor: docker 106 | tag: build_host 107 | mysecondhost: 108 | dockerfile: path/to/file # file references build_host 109 | platform: alpine-3.8-x86_64 110 | hypervisor: docker 111 | CONFIG: 112 | type: foss 113 | 114 | ### Reuse Docker Image ### 115 | In case you want to rerun the puppet again on the docker container, you can pass BEAKER_provision=no on the command line to set the env. Add this line in you default.ml file 116 | 117 | ``` 118 | HOSTS: 119 | centos6-64: 120 | roles: 121 | - agent 122 | platform: el-6-x86_64 123 | image: centos:6.6 124 | hypervisor: docker 125 | CONFIG: 126 | type: foss 127 | log_level: verbose 128 | ssh: 129 | password: root 130 | auth_methods: ["password"] 131 | ``` 132 | 133 | ### Mounting volumes into your docker container ### 134 | You can mount folders into a docker container: 135 | 136 | HOSTS: 137 | ubuntu-12-10: 138 | platform: ubuntu-12.10-x64 139 | image: ubuntu:12.10 140 | hypervisor: docker 141 | mount_folders: 142 | name1: 143 | host_path: host_path1 144 | container_path: container_path1 145 | name2: 146 | host_path: host_path2 147 | container_path: container_path2 148 | opts: rw 149 | CONFIG: 150 | type: foss 151 | 152 | ### Example Output 153 | 154 | For this example made a new docker nodeset file in the [puppetlabs-inifile](https://github.com/puppetlabs/puppetlabs-inifile) repo and ran the ini_setting_spec.rb spec: 155 | 156 | ```bash 157 | $ bundle exec rspec spec/acceptance/ini_setting_spec.rb 158 | Hypervisor for debian-7 is docker 159 | Beaker::Hypervisor, found some docker boxes to create 160 | Provisioning docker 161 | provisioning debian-7 162 | Creating image 163 | Dockerfile is FROM debian:7.4 164 | RUN apt-get update 165 | RUN apt-get install -y openssh-server openssh-client curl ntpdate lsb-release 166 | RUN mkdir -p /var/run/sshd 167 | RUN echo root:root | chpasswd 168 | RUN apt-get install -yq lsb-release wget net-tools ruby rubygems ruby1.8-dev libaugeas-dev libaugeas-ruby ntpdate locales-all 169 | RUN REALLY_GEM_UPDATE_SYSTEM=1 gem update --system --no-ri --no-rdoc 170 | EXPOSE 22 171 | CMD ["/sbin/init"] 172 | ``` 173 | 174 | This step may take a while, as Docker will have to download the image. The subsequent runs will be a lot faster (as long as `docker_preserve_image: true` has been enabled). 175 | 176 | For example, running this took 5 minutes to download and setup the `debian:7.4` image, but runs instantly the second time. 177 | 178 | You should then see something like: 179 | 180 | ``` 181 | Creating container from image 3a86e5aba94d 182 | post 183 | /v1.15/containers/create 184 | {} 185 | {"Image":"3a86e5aba94d","Hostname":"debian-7"} 186 | Starting container b8b31702b34b4aedd137c8a6a72fe730560bb00533e68764ba6263405f9244e4 187 | post 188 | /v1.15/containers/b8b31702b34b4aedd137c8a6a72fe730560bb00533e68764ba6263405f9244e4/start 189 | {} 190 | {"PublishAllPorts":true,"Privileged":true} 191 | Using docker server at 192.168.59.103 192 | get 193 | /v1.15/containers/b8b31702b34b4aedd137c8a6a72fe730560bb00533e68764ba6263405f9244e4/json 194 | {} 195 | 196 | node available as ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@192.168.59.103 -p 49155 197 | ``` 198 | 199 | The tests should then run as normal from there. 200 | 201 | 202 | ### Docker-in-Docker (dind) ### 203 | If you are using docker in docker, set the environment variable DOCKER_IN_DOCKER=true. Beaker-docker will then not try to use the DOCKER_HOST address for the ssh connection to the containers. 204 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [3.0.1](https://github.com/voxpupuli/beaker-docker/tree/3.0.1) (2025-09-30) 4 | 5 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/3.0.0...3.0.1) 6 | 7 | **Fixed bugs:** 8 | 9 | - fix: deep merge dockeropts [\#167](https://github.com/voxpupuli/beaker-docker/pull/167) ([mpldr](https://github.com/mpldr)) 10 | 11 | ## [3.0.0](https://github.com/voxpupuli/beaker-docker/tree/3.0.0) (2025-08-11) 12 | 13 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.6.0...3.0.0) 14 | 15 | **Breaking changes:** 16 | 17 | - Require Ruby 3.2 or newer [\#163](https://github.com/voxpupuli/beaker-docker/pull/163) ([bastelfreak](https://github.com/bastelfreak)) 18 | 19 | **Implemented enhancements:** 20 | 21 | - beaker: Allow 7.x [\#162](https://github.com/voxpupuli/beaker-docker/pull/162) ([bastelfreak](https://github.com/bastelfreak)) 22 | 23 | ## [2.6.0](https://github.com/voxpupuli/beaker-docker/tree/2.6.0) (2025-05-28) 24 | 25 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.5.2...2.6.0) 26 | 27 | **Implemented enhancements:** 28 | 29 | - CI: Run tests with podman and docker [\#160](https://github.com/voxpupuli/beaker-docker/pull/160) ([bastelfreak](https://github.com/bastelfreak)) 30 | - CI: Generate matrix automatically & drop unused coverage reporting [\#158](https://github.com/voxpupuli/beaker-docker/pull/158) ([bastelfreak](https://github.com/bastelfreak)) 31 | - voxpupuli-rubocop: 3.0.0-\>3.1.0 [\#157](https://github.com/voxpupuli/beaker-docker/pull/157) ([bastelfreak](https://github.com/bastelfreak)) 32 | - fakefs: Allow 3.x [\#155](https://github.com/voxpupuli/beaker-docker/pull/155) ([bastelfreak](https://github.com/bastelfreak)) 33 | 34 | **Fixed bugs:** 35 | 36 | - excon: exclude 1.2.6 [\#156](https://github.com/voxpupuli/beaker-docker/pull/156) ([bastelfreak](https://github.com/bastelfreak)) 37 | 38 | ## [2.5.2](https://github.com/voxpupuli/beaker-docker/tree/2.5.2) (2024-11-20) 39 | 40 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.5.1...2.5.2) 41 | 42 | **Fixed bugs:** 43 | 44 | - fix: ignore /etc/ssh/sshd\_config.d/\* if files are not present [\#152](https://github.com/voxpupuli/beaker-docker/pull/152) ([vchepkov](https://github.com/vchepkov)) 45 | 46 | ## [2.5.1](https://github.com/voxpupuli/beaker-docker/tree/2.5.1) (2024-11-20) 47 | 48 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.5.0...2.5.1) 49 | 50 | **Fixed bugs:** 51 | 52 | - Update sshd configuration \(UsePAM no, sshd\_config.d\) [\#148](https://github.com/voxpupuli/beaker-docker/pull/148) ([jay7x](https://github.com/jay7x)) 53 | 54 | ## [2.5.0](https://github.com/voxpupuli/beaker-docker/tree/2.5.0) (2024-09-20) 55 | 56 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.4.0...2.5.0) 57 | 58 | **Implemented enhancements:** 59 | 60 | - add ContainerDocker and ContainerSwarm [\#145](https://github.com/voxpupuli/beaker-docker/pull/145) ([evgeni](https://github.com/evgeni)) 61 | - Add ContainerPodman class [\#130](https://github.com/voxpupuli/beaker-docker/pull/130) ([bastelfreak](https://github.com/bastelfreak)) 62 | 63 | **Merged pull requests:** 64 | 65 | - fix tests after Beaker::Platform refactoring [\#146](https://github.com/voxpupuli/beaker-docker/pull/146) ([evgeni](https://github.com/evgeni)) 66 | 67 | ## [2.4.0](https://github.com/voxpupuli/beaker-docker/tree/2.4.0) (2024-08-13) 68 | 69 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.3.1...2.4.0) 70 | 71 | **Implemented enhancements:** 72 | 73 | - Arch Linux: remove redundant system updates [\#142](https://github.com/voxpupuli/beaker-docker/pull/142) ([bastelfreak](https://github.com/bastelfreak)) 74 | 75 | ## [2.3.1](https://github.com/voxpupuli/beaker-docker/tree/2.3.1) (2024-07-08) 76 | 77 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.3.0...2.3.1) 78 | 79 | **Fixed bugs:** 80 | 81 | - CI: Switch from EL7/8-\>Debian12/EL9 [\#140](https://github.com/voxpupuli/beaker-docker/pull/140) ([bastelfreak](https://github.com/bastelfreak)) 82 | - docker-api: depend on 2.3 or newer [\#139](https://github.com/voxpupuli/beaker-docker/pull/139) ([bastelfreak](https://github.com/bastelfreak)) 83 | 84 | **Merged pull requests:** 85 | 86 | - voxpupuli-rubocop: Update to 2.8.0 [\#138](https://github.com/voxpupuli/beaker-docker/pull/138) ([bastelfreak](https://github.com/bastelfreak)) 87 | 88 | ## [2.3.0](https://github.com/voxpupuli/beaker-docker/tree/2.3.0) (2024-05-28) 89 | 90 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.2.1...2.3.0) 91 | 92 | **Implemented enhancements:** 93 | 94 | - beaker: Allow 6.x [\#135](https://github.com/voxpupuli/beaker-docker/pull/135) ([bastelfreak](https://github.com/bastelfreak)) 95 | - Add Ruby 3.3 to CI matrix [\#134](https://github.com/voxpupuli/beaker-docker/pull/134) ([bastelfreak](https://github.com/bastelfreak)) 96 | 97 | **Merged pull requests:** 98 | 99 | - voxpupuli-rubocop: Use 2.7.0 [\#136](https://github.com/voxpupuli/beaker-docker/pull/136) ([bastelfreak](https://github.com/bastelfreak)) 100 | 101 | ## [2.2.1](https://github.com/voxpupuli/beaker-docker/tree/2.2.1) (2024-03-27) 102 | 103 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.2.0...2.2.1) 104 | 105 | **Fixed bugs:** 106 | 107 | - Fix privileged remote docker port forward resolution [\#126](https://github.com/voxpupuli/beaker-docker/pull/126) ([h0tw1r3](https://github.com/h0tw1r3)) 108 | 109 | ## [2.2.0](https://github.com/voxpupuli/beaker-docker/tree/2.2.0) (2024-03-17) 110 | 111 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.1.0...2.2.0) 112 | 113 | **Implemented enhancements:** 114 | 115 | - Implement writing out a containerfile for a host [\#125](https://github.com/voxpupuli/beaker-docker/pull/125) ([ekohl](https://github.com/ekohl)) 116 | - Support Amazon Linux 2023 [\#123](https://github.com/voxpupuli/beaker-docker/pull/123) ([treydock](https://github.com/treydock)) 117 | 118 | **Merged pull requests:** 119 | 120 | - Rakefile: Use rubocop tasks from voxpupuli-rubocop [\#131](https://github.com/voxpupuli/beaker-docker/pull/131) ([bastelfreak](https://github.com/bastelfreak)) 121 | - voxpupuli-rubocop: Update 1.2-\>2.6 [\#129](https://github.com/voxpupuli/beaker-docker/pull/129) ([bastelfreak](https://github.com/bastelfreak)) 122 | - README.md: Document CLI; Add ToC [\#127](https://github.com/voxpupuli/beaker-docker/pull/127) ([bastelfreak](https://github.com/bastelfreak)) 123 | 124 | ## [2.1.0](https://github.com/voxpupuli/beaker-docker/tree/2.1.0) (2023-05-05) 125 | 126 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/2.0.0...2.1.0) 127 | 128 | **Implemented enhancements:** 129 | 130 | - switch to voxpupuli-rubocop & restore beaker 4 support [\#120](https://github.com/voxpupuli/beaker-docker/pull/120) ([bastelfreak](https://github.com/bastelfreak)) 131 | 132 | **Closed issues:** 133 | 134 | - Enable SSH Agent forwarding on MacOS [\#117](https://github.com/voxpupuli/beaker-docker/issues/117) 135 | 136 | **Merged pull requests:** 137 | 138 | - GCG: Add faraday-retry dep [\#119](https://github.com/voxpupuli/beaker-docker/pull/119) ([bastelfreak](https://github.com/bastelfreak)) 139 | - CI: Build gems with strictness and verbosity [\#118](https://github.com/voxpupuli/beaker-docker/pull/118) ([bastelfreak](https://github.com/bastelfreak)) 140 | - rubocop: Fix RSpec/AnyInstance [\#116](https://github.com/voxpupuli/beaker-docker/pull/116) ([jay7x](https://github.com/jay7x)) 141 | - rubocop: Fix RSpec/VerifiedDoubles [\#115](https://github.com/voxpupuli/beaker-docker/pull/115) ([jay7x](https://github.com/jay7x)) 142 | 143 | ## [2.0.0](https://github.com/voxpupuli/beaker-docker/tree/2.0.0) (2023-03-28) 144 | 145 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/1.5.0...2.0.0) 146 | 147 | **Breaking changes:** 148 | 149 | - Drop Ruby 2.4/2.5/2.6 support [\#109](https://github.com/voxpupuli/beaker-docker/pull/109) ([bastelfreak](https://github.com/bastelfreak)) 150 | 151 | **Implemented enhancements:** 152 | 153 | - Drop `beaker-rspec` dependency [\#107](https://github.com/voxpupuli/beaker-docker/pull/107) ([jay7x](https://github.com/jay7x)) 154 | 155 | **Merged pull requests:** 156 | 157 | - Rubocop: fix more violations [\#113](https://github.com/voxpupuli/beaker-docker/pull/113) ([bastelfreak](https://github.com/bastelfreak)) 158 | - rubocop: Use shared config from beaker [\#112](https://github.com/voxpupuli/beaker-docker/pull/112) ([bastelfreak](https://github.com/bastelfreak)) 159 | - Fix even more rubocop issues [\#111](https://github.com/voxpupuli/beaker-docker/pull/111) ([jay7x](https://github.com/jay7x)) 160 | - More rubocop fixes [\#110](https://github.com/voxpupuli/beaker-docker/pull/110) ([jay7x](https://github.com/jay7x)) 161 | - Fix more rubocop warnings [\#108](https://github.com/voxpupuli/beaker-docker/pull/108) ([jay7x](https://github.com/jay7x)) 162 | - Fix multiple Rubocop warnings [\#106](https://github.com/voxpupuli/beaker-docker/pull/106) ([bastelfreak](https://github.com/bastelfreak)) 163 | 164 | ## [1.5.0](https://github.com/voxpupuli/beaker-docker/tree/1.5.0) (2023-03-24) 165 | 166 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/1.4.0...1.5.0) 167 | 168 | **Implemented enhancements:** 169 | 170 | - Ruby 3.2 compatibility [\#100](https://github.com/voxpupuli/beaker-docker/pull/100) ([ekohl](https://github.com/ekohl)) 171 | - Set required Ruby version to 2.4+ [\#99](https://github.com/voxpupuli/beaker-docker/pull/99) ([ekohl](https://github.com/ekohl)) 172 | - Simplify port detection code [\#95](https://github.com/voxpupuli/beaker-docker/pull/95) ([ekohl](https://github.com/ekohl)) 173 | - Add Ruby 3.1 to CI matrix [\#87](https://github.com/voxpupuli/beaker-docker/pull/87) ([bastelfreak](https://github.com/bastelfreak)) 174 | - Use ssh-keygen -A on Red Hat-based distros & SuSE/SLES [\#73](https://github.com/voxpupuli/beaker-docker/pull/73) ([ekohl](https://github.com/ekohl)) 175 | 176 | **Fixed bugs:** 177 | 178 | - Deal with docker\_cmd being an array and remove use of =~ [\#93](https://github.com/voxpupuli/beaker-docker/pull/93) ([ekohl](https://github.com/ekohl)) 179 | 180 | **Merged pull requests:** 181 | 182 | - Remove Gemfile.local from git [\#104](https://github.com/voxpupuli/beaker-docker/pull/104) ([ekohl](https://github.com/ekohl)) 183 | - Fix rubocop Naming/FileName [\#103](https://github.com/voxpupuli/beaker-docker/pull/103) ([jay7x](https://github.com/jay7x)) 184 | - cleanup GitHub actions [\#102](https://github.com/voxpupuli/beaker-docker/pull/102) ([bastelfreak](https://github.com/bastelfreak)) 185 | - Remove unused rspec-its dependency [\#98](https://github.com/voxpupuli/beaker-docker/pull/98) ([ekohl](https://github.com/ekohl)) 186 | - Allow fakefs 2.x [\#97](https://github.com/voxpupuli/beaker-docker/pull/97) ([ekohl](https://github.com/ekohl)) 187 | - Remove yard rake tasks [\#96](https://github.com/voxpupuli/beaker-docker/pull/96) ([ekohl](https://github.com/ekohl)) 188 | - rubocop: fix dependency ordering [\#94](https://github.com/voxpupuli/beaker-docker/pull/94) ([bastelfreak](https://github.com/bastelfreak)) 189 | - GHA: Use builtin podman [\#86](https://github.com/voxpupuli/beaker-docker/pull/86) ([bastelfreak](https://github.com/bastelfreak)) 190 | - GHA: Use builtin docker [\#85](https://github.com/voxpupuli/beaker-docker/pull/85) ([bastelfreak](https://github.com/bastelfreak)) 191 | - Fix rubocop-related issues \(part 1\) [\#75](https://github.com/voxpupuli/beaker-docker/pull/75) ([jay7x](https://github.com/jay7x)) 192 | 193 | ## [1.4.0](https://github.com/voxpupuli/beaker-docker/tree/1.4.0) (2023-03-10) 194 | 195 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/1.3.0...1.4.0) 196 | 197 | **Implemented enhancements:** 198 | 199 | - Enable Rubocop [\#72](https://github.com/voxpupuli/beaker-docker/pull/72) ([jay7x](https://github.com/jay7x)) 200 | - Refactor built-in Dockerfile and fix\_ssh\(\) [\#71](https://github.com/voxpupuli/beaker-docker/pull/71) ([jay7x](https://github.com/jay7x)) 201 | 202 | **Fixed bugs:** 203 | 204 | - set flag for container to container communication [\#84](https://github.com/voxpupuli/beaker-docker/pull/84) ([rwaffen](https://github.com/rwaffen)) 205 | 206 | **Merged pull requests:** 207 | 208 | - dependabot: check for github actions as well [\#89](https://github.com/voxpupuli/beaker-docker/pull/89) ([bastelfreak](https://github.com/bastelfreak)) 209 | 210 | ## [1.3.0](https://github.com/voxpupuli/beaker-docker/tree/1.3.0) (2022-12-18) 211 | 212 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/1.2.0...1.3.0) 213 | 214 | **Implemented enhancements:** 215 | 216 | - Generate a ssh port from 1025..9999 range [\#68](https://github.com/voxpupuli/beaker-docker/pull/68) ([jay7x](https://github.com/jay7x)) 217 | 218 | ## [1.2.0](https://github.com/voxpupuli/beaker-docker/tree/1.2.0) (2022-08-11) 219 | 220 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/1.1.1...1.2.0) 221 | 222 | **Implemented enhancements:** 223 | 224 | - Use ssh-keygen -A on modern Enterprise Linux [\#66](https://github.com/voxpupuli/beaker-docker/pull/66) ([ekohl](https://github.com/ekohl)) 225 | - Add Docker hostfile parameter docker\_image\_first\_commands [\#65](https://github.com/voxpupuli/beaker-docker/pull/65) ([Rathios](https://github.com/Rathios)) 226 | 227 | ## [1.1.1](https://github.com/voxpupuli/beaker-docker/tree/1.1.1) (2022-02-17) 228 | 229 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/1.1.0...1.1.1) 230 | 231 | **Fixed bugs:** 232 | 233 | - Arch Linux: do not install openssh twice [\#58](https://github.com/voxpupuli/beaker-docker/pull/58) ([bastelfreak](https://github.com/bastelfreak)) 234 | 235 | **Merged pull requests:** 236 | 237 | - Remove beaker from Gemfile [\#62](https://github.com/voxpupuli/beaker-docker/pull/62) ([bastelfreak](https://github.com/bastelfreak)) 238 | - CI: Switch centos:8 to centos:stream8 image [\#61](https://github.com/voxpupuli/beaker-docker/pull/61) ([bastelfreak](https://github.com/bastelfreak)) 239 | 240 | ## [1.1.0](https://github.com/voxpupuli/beaker-docker/tree/1.1.0) (2022-01-27) 241 | 242 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/1.0.1...1.1.0) 243 | 244 | **Implemented enhancements:** 245 | 246 | - Use host\_packages helper to reuse logic from beaker [\#59](https://github.com/voxpupuli/beaker-docker/pull/59) ([ekohl](https://github.com/ekohl)) 247 | 248 | ## [1.0.1](https://github.com/voxpupuli/beaker-docker/tree/1.0.1) (2021-09-13) 249 | 250 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/1.0.0...1.0.1) 251 | 252 | **Implemented enhancements:** 253 | 254 | - Initial EL9 support [\#55](https://github.com/voxpupuli/beaker-docker/pull/55) ([ekohl](https://github.com/ekohl)) 255 | - Add support for additional Docker port bindings [\#54](https://github.com/voxpupuli/beaker-docker/pull/54) ([treydock](https://github.com/treydock)) 256 | 257 | **Fixed bugs:** 258 | 259 | - Fix IP detection in WSL2 environments [\#56](https://github.com/voxpupuli/beaker-docker/pull/56) ([trevor-vaughan](https://github.com/trevor-vaughan)) 260 | - Fix SSH port binding [\#53](https://github.com/voxpupuli/beaker-docker/pull/53) ([treydock](https://github.com/treydock)) 261 | - Added ENV DOCKER\_IN\_DOCKER to fix SSH conn info [\#51](https://github.com/voxpupuli/beaker-docker/pull/51) ([QueerCodingGirl](https://github.com/QueerCodingGirl)) 262 | 263 | **Closed issues:** 264 | 265 | - Regression with 1.0.0 WRT SSH port usage [\#52](https://github.com/voxpupuli/beaker-docker/issues/52) 266 | 267 | ## [1.0.0](https://github.com/voxpupuli/beaker-docker/tree/1.0.0) (2021-08-06) 268 | 269 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.8.4...1.0.0) 270 | 271 | **Implemented enhancements:** 272 | 273 | - Implement codecov reporting [\#49](https://github.com/voxpupuli/beaker-docker/pull/49) ([bastelfreak](https://github.com/bastelfreak)) 274 | 275 | **Fixed bugs:** 276 | 277 | - Treat Fedora 22+ and EL8 the same [\#48](https://github.com/voxpupuli/beaker-docker/pull/48) ([ekohl](https://github.com/ekohl)) 278 | - Be more aggressive about picking a connection [\#47](https://github.com/voxpupuli/beaker-docker/pull/47) ([trevor-vaughan](https://github.com/trevor-vaughan)) 279 | 280 | ## [0.8.4](https://github.com/voxpupuli/beaker-docker/tree/0.8.4) (2021-03-15) 281 | 282 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.8.3...0.8.4) 283 | 284 | **Fixed bugs:** 285 | 286 | - Force Container Removal [\#45](https://github.com/voxpupuli/beaker-docker/pull/45) ([trevor-vaughan](https://github.com/trevor-vaughan)) 287 | 288 | **Closed issues:** 289 | 290 | - Wrong SSH port getting used [\#43](https://github.com/voxpupuli/beaker-docker/issues/43) 291 | - Beaker complains about host unreachable - Ubuntu 18 and 20 [\#39](https://github.com/voxpupuli/beaker-docker/issues/39) 292 | 293 | **Merged pull requests:** 294 | 295 | - Fix docker usage to use correct port and IP address on local docker [\#44](https://github.com/voxpupuli/beaker-docker/pull/44) ([treydock](https://github.com/treydock)) 296 | - Update to Check Rootless [\#41](https://github.com/voxpupuli/beaker-docker/pull/41) ([trevor-vaughan](https://github.com/trevor-vaughan)) 297 | - Change from my personal fork to docker-api 2.1+ [\#40](https://github.com/voxpupuli/beaker-docker/pull/40) ([trevor-vaughan](https://github.com/trevor-vaughan)) 298 | 299 | ## [0.8.3](https://github.com/voxpupuli/beaker-docker/tree/0.8.3) (2021-02-28) 300 | 301 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.8.2...0.8.3) 302 | 303 | **Merged pull requests:** 304 | 305 | - Cleanup docs and gemspec [\#37](https://github.com/voxpupuli/beaker-docker/pull/37) ([genebean](https://github.com/genebean)) 306 | 307 | ## [0.8.2](https://github.com/voxpupuli/beaker-docker/tree/0.8.2) (2021-02-28) 308 | 309 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.8.1...0.8.2) 310 | 311 | **Merged pull requests:** 312 | 313 | - Deconflict Privileged and CAPs [\#34](https://github.com/voxpupuli/beaker-docker/pull/34) ([trevor-vaughan](https://github.com/trevor-vaughan)) 314 | 315 | ## [0.8.1](https://github.com/voxpupuli/beaker-docker/tree/0.8.1) (2021-02-28) 316 | 317 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.8.0...0.8.1) 318 | 319 | **Merged pull requests:** 320 | 321 | - Fix docker support and update github actions [\#32](https://github.com/voxpupuli/beaker-docker/pull/32) ([trevor-vaughan](https://github.com/trevor-vaughan)) 322 | - Add GH Action for releases [\#31](https://github.com/voxpupuli/beaker-docker/pull/31) ([genebean](https://github.com/genebean)) 323 | 324 | ## [0.8.0](https://github.com/voxpupuli/beaker-docker/tree/0.8.0) (2021-02-26) 325 | 326 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.7.1...0.8.0) 327 | 328 | **Merged pull requests:** 329 | 330 | - Move testing to GH Actions [\#30](https://github.com/voxpupuli/beaker-docker/pull/30) ([genebean](https://github.com/genebean)) 331 | - Add Podman Support [\#29](https://github.com/voxpupuli/beaker-docker/pull/29) ([trevor-vaughan](https://github.com/trevor-vaughan)) 332 | 333 | ## [0.7.1](https://github.com/voxpupuli/beaker-docker/tree/0.7.1) (2020-09-11) 334 | 335 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.7.0...0.7.1) 336 | 337 | **Merged pull requests:** 338 | 339 | - Fix: docker-api gem dependency [\#26](https://github.com/voxpupuli/beaker-docker/pull/26) ([msalway](https://github.com/msalway)) 340 | - Add Dependabot to keep thins up to date [\#23](https://github.com/voxpupuli/beaker-docker/pull/23) ([genebean](https://github.com/genebean)) 341 | 342 | ## [0.7.0](https://github.com/voxpupuli/beaker-docker/tree/0.7.0) (2020-01-23) 343 | 344 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.6.0...0.7.0) 345 | 346 | **Merged pull requests:** 347 | 348 | - Fix: Too many authentication failures [\#21](https://github.com/voxpupuli/beaker-docker/pull/21) ([b4ldr](https://github.com/b4ldr)) 349 | - \(MAINT\) add release section to README [\#20](https://github.com/voxpupuli/beaker-docker/pull/20) ([kevpl](https://github.com/kevpl)) 350 | 351 | ## [0.6.0](https://github.com/voxpupuli/beaker-docker/tree/0.6.0) (2019-11-12) 352 | 353 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.5.4...0.6.0) 354 | 355 | **Merged pull requests:** 356 | 357 | - \(BKR-1613\) add CentOS8 support [\#19](https://github.com/voxpupuli/beaker-docker/pull/19) ([ciprianbadescu](https://github.com/ciprianbadescu)) 358 | 359 | ## [0.5.4](https://github.com/voxpupuli/beaker-docker/tree/0.5.4) (2019-07-15) 360 | 361 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.5.3...0.5.4) 362 | 363 | **Merged pull requests:** 364 | 365 | - \(maint\) A number of fixes for rerunning tests on docker containers [\#18](https://github.com/voxpupuli/beaker-docker/pull/18) ([underscorgan](https://github.com/underscorgan)) 366 | 367 | ## [0.5.3](https://github.com/voxpupuli/beaker-docker/tree/0.5.3) (2019-05-06) 368 | 369 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.5.2...0.5.3) 370 | 371 | **Merged pull requests:** 372 | 373 | - BKR-1586 - allow an 'as-is' container to be used rather than rebuilding every time [\#17](https://github.com/voxpupuli/beaker-docker/pull/17) ([oldNoakes](https://github.com/oldNoakes)) 374 | 375 | ## [0.5.2](https://github.com/voxpupuli/beaker-docker/tree/0.5.2) (2019-02-11) 376 | 377 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.5.1...0.5.2) 378 | 379 | **Merged pull requests:** 380 | 381 | - Allow users with large keyrings to run test [\#16](https://github.com/voxpupuli/beaker-docker/pull/16) ([trevor-vaughan](https://github.com/trevor-vaughan)) 382 | 383 | ## [0.5.1](https://github.com/voxpupuli/beaker-docker/tree/0.5.1) (2018-11-29) 384 | 385 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.5.0...0.5.1) 386 | 387 | **Merged pull requests:** 388 | 389 | - \(SERVER-2380\) add image tagging ability [\#14](https://github.com/voxpupuli/beaker-docker/pull/14) ([tvpartytonight](https://github.com/tvpartytonight)) 390 | 391 | ## [0.5.0](https://github.com/voxpupuli/beaker-docker/tree/0.5.0) (2018-11-19) 392 | 393 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.4.0...0.5.0) 394 | 395 | **Merged pull requests:** 396 | 397 | - \(BKR-1551\) Updates for Beaker 4 [\#13](https://github.com/voxpupuli/beaker-docker/pull/13) ([caseywilliams](https://github.com/caseywilliams)) 398 | 399 | ## [0.4.0](https://github.com/voxpupuli/beaker-docker/tree/0.4.0) (2018-10-26) 400 | 401 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.3.3...0.4.0) 402 | 403 | **Merged pull requests:** 404 | 405 | - \(PUP-9212\) Allow for building containers with context [\#12](https://github.com/voxpupuli/beaker-docker/pull/12) ([tvpartytonight](https://github.com/tvpartytonight)) 406 | - \(PUP-9212\) Allow for image entry point CMDs [\#11](https://github.com/voxpupuli/beaker-docker/pull/11) ([tvpartytonight](https://github.com/tvpartytonight)) 407 | - \(BKR-1509\) Hypervisor usage instructions for Beaker 4..0 [\#9](https://github.com/voxpupuli/beaker-docker/pull/9) ([Dakta](https://github.com/Dakta)) 408 | 409 | ## [0.3.3](https://github.com/voxpupuli/beaker-docker/tree/0.3.3) (2018-04-16) 410 | 411 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.3.2...0.3.3) 412 | 413 | **Merged pull requests:** 414 | 415 | - \(BKR-305\) Support custom docker options [\#8](https://github.com/voxpupuli/beaker-docker/pull/8) ([double16](https://github.com/double16)) 416 | 417 | ## [0.3.2](https://github.com/voxpupuli/beaker-docker/tree/0.3.2) (2018-04-09) 418 | 419 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.3.1...0.3.2) 420 | 421 | **Merged pull requests:** 422 | 423 | - \(MAINT\) fix paths when using DOCKER\_TOOLBOX on windows [\#7](https://github.com/voxpupuli/beaker-docker/pull/7) ([tabakhase](https://github.com/tabakhase)) 424 | 425 | ## [0.3.1](https://github.com/voxpupuli/beaker-docker/tree/0.3.1) (2018-02-22) 426 | 427 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.3.0...0.3.1) 428 | 429 | **Merged pull requests:** 430 | 431 | - Fix Archlinux support [\#6](https://github.com/voxpupuli/beaker-docker/pull/6) ([bastelfreak](https://github.com/bastelfreak)) 432 | 433 | ## [0.3.0](https://github.com/voxpupuli/beaker-docker/tree/0.3.0) (2018-01-29) 434 | 435 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.2.0...0.3.0) 436 | 437 | **Merged pull requests:** 438 | 439 | - \[BKR-1021\] Archlinux support [\#5](https://github.com/voxpupuli/beaker-docker/pull/5) ([jantman](https://github.com/jantman)) 440 | - Don't set container name to node hostname [\#4](https://github.com/voxpupuli/beaker-docker/pull/4) ([jovrum](https://github.com/jovrum)) 441 | 442 | ## [0.2.0](https://github.com/voxpupuli/beaker-docker/tree/0.2.0) (2017-08-11) 443 | 444 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/0.1.0...0.2.0) 445 | 446 | **Merged pull requests:** 447 | 448 | - \(BKR-1189\) Fix port mapping [\#3](https://github.com/voxpupuli/beaker-docker/pull/3) ([rishijavia](https://github.com/rishijavia)) 449 | - Make beaker-docker work in a docker container [\#2](https://github.com/voxpupuli/beaker-docker/pull/2) ([hedinasr](https://github.com/hedinasr)) 450 | 451 | ## [0.1.0](https://github.com/voxpupuli/beaker-docker/tree/0.1.0) (2017-08-01) 452 | 453 | [Full Changelog](https://github.com/voxpupuli/beaker-docker/compare/7f6a78541f30385016478e810ecb0c14f3936e20...0.1.0) 454 | 455 | **Merged pull requests:** 456 | 457 | - \(MAINT\) Add docker-api dependency as its removed from beaker [\#1](https://github.com/voxpupuli/beaker-docker/pull/1) ([rishijavia](https://github.com/rishijavia)) 458 | 459 | 460 | 461 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 462 | -------------------------------------------------------------------------------- /lib/beaker/hypervisor/docker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Beaker 4 | # Docker hypervisor for Beaker acceptance testing framework 5 | class Docker < Beaker::Hypervisor 6 | # Docker hypvervisor initializtion 7 | # Env variables supported: 8 | # DOCKER_REGISTRY: Docker registry URL 9 | # DOCKER_HOST: Remote docker host 10 | # DOCKER_BUILDARGS: Docker buildargs map 11 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 12 | # or a role (String or Symbol) that identifies one or more hosts. 13 | # @param [Hash{Symbol=>String}] options Options to pass on to the hypervisor 14 | def initialize(hosts, options) 15 | super 16 | require 'docker' 17 | @options = options 18 | @logger = options[:logger] || Beaker::Logger.new 19 | @hosts = hosts 20 | 21 | # increase the http timeouts as provisioning images can be slow 22 | default_docker_options = { write_timeout: 300, read_timeout: 300 }.merge(::Docker.options || {}) 23 | # Merge docker options from the entry in hosts file 24 | ::Docker.options = default_docker_options.merge(@options[:docker_options] || {}) 25 | 26 | # Ensure that we can correctly communicate with the docker API 27 | begin 28 | @docker_version = ::Docker.version 29 | rescue Excon::Errors::SocketError => e 30 | raise <<~ERRMSG 31 | Docker instance not connectable 32 | Error was: #{e} 33 | * Check your DOCKER_HOST variable has been set 34 | * If you are on OSX or Windows, you might not have Docker Machine setup correctly: https://docs.docker.com/machine/ 35 | * If you are using rootless podman, you might need to set up your local socket and service 36 | ERRMSG 37 | end 38 | 39 | # Pass on all the logging from docker-api to the beaker logger instance 40 | ::Docker.logger = @logger 41 | 42 | # Find out what kind of remote instance we are talking against 43 | if @docker_version['Version'].include?('swarm') 44 | @docker_type = 'swarm' 45 | raise "Using Swarm with beaker requires a private registry. Please setup the private registry and set the 'DOCKER_REGISTRY' env var" unless ENV['DOCKER_REGISTRY'] 46 | 47 | @registry = ENV.fetch('DOCKER_REGISTRY', nil) 48 | 49 | elsif ::Docker.respond_to?(:podman?) && ::Docker.podman? 50 | @docker_type = 'podman' 51 | else 52 | @docker_type = 'docker' 53 | end 54 | end 55 | 56 | def install_and_run_ssh(host) 57 | def host.enable_root_login(host, _opts) 58 | logger.debug("Root login already enabled for #{host}") 59 | end 60 | 61 | # If the container is running ssh as its init process then this method 62 | # will cause issues. 63 | if Array(host[:docker_cmd]).first&.include?('sshd') 64 | def host.ssh_service_restart 65 | self[:docker_container].exec(%w[kill -1 1]) 66 | end 67 | end 68 | 69 | host['dockerfile'] || host['use_image_entry_point'] 70 | end 71 | 72 | def get_container_opts(host, image_name) 73 | container_opts = StringifyHash.new 74 | container_opts['ExposedPorts'] = { '22/tcp' => {} } if host['dockerfile'] 75 | 76 | container_opts.merge!({ 77 | 'Image' => image_name, 78 | 'Hostname' => host.name, 79 | 'HostConfig' => { 80 | 'PortBindings' => { 81 | '22/tcp' => [{ 'HostPort' => rand(1025..9999).to_s, 'HostIp' => '0.0.0.0' }], 82 | }, 83 | 'PublishAllPorts' => true, 84 | 'RestartPolicy' => { 85 | 'Name' => 'always', 86 | }, 87 | }, 88 | }) 89 | end 90 | 91 | def get_container_image(host) 92 | @logger.debug('Creating image') 93 | 94 | return ::Docker::Image.create('fromImage' => host['image']) if host['use_image_as_is'] 95 | 96 | dockerfile = host['dockerfile'] 97 | if dockerfile 98 | # assume that the dockerfile is in the repo and tests are running 99 | # from the root of the repo; maybe add support for external Dockerfiles 100 | # with external build dependencies later. 101 | raise "Unable to find dockerfile at #{dockerfile}" unless File.exist?(dockerfile) 102 | 103 | dir = File.expand_path(dockerfile).chomp(dockerfile) 104 | return ::Docker::Image.build_from_dir( 105 | dir, 106 | { 107 | 'dockerfile' => dockerfile, 108 | :rm => true, 109 | :buildargs => buildargs_for(host), 110 | }, 111 | ) 112 | 113 | elsif host['use_image_entry_point'] 114 | df = <<-DF 115 | FROM #{host['image']} 116 | EXPOSE 22 117 | DF 118 | 119 | cmd = host['docker_cmd'] 120 | df += cmd if cmd 121 | return ::Docker::Image.build(df, { rm: true, buildargs: buildargs_for(host) }) 122 | end 123 | 124 | ::Docker::Image.build(dockerfile_for(host), { rm: true, buildargs: buildargs_for(host) }) 125 | end 126 | 127 | # Nested Docker scenarios 128 | def nested_docker? 129 | ENV['DOCKER_IN_DOCKER'] || ENV.fetch('WSLENV', nil) 130 | end 131 | 132 | # Find out where the ssh port is from the container 133 | # When running on swarm DOCKER_HOST points to the swarm manager so we have to get the 134 | # IP of the swarm slave via the container data 135 | # When we are talking to a normal docker instance DOCKER_HOST can point to a remote docker instance. 136 | def get_ssh_connection_info(container) 137 | ssh_connection_info = { 138 | ip: nil, 139 | port: nil, 140 | } 141 | 142 | container_json = container.json 143 | network_settings = container_json['NetworkSettings'] 144 | host_config = container_json['HostConfig'] 145 | 146 | ip = nil 147 | port = nil 148 | # Talking against a remote docker host which is a normal docker host 149 | if @docker_type == 'docker' && ENV.fetch('DOCKER_HOST', nil) && !ENV.fetch('DOCKER_HOST', '').include?(':///') && !nested_docker? 150 | ip = URI.parse(ENV.fetch('DOCKER_HOST', nil)).host 151 | elsif in_container? && !nested_docker? 152 | # Swarm or local docker host 153 | gw = network_settings['Gateway'] 154 | ip = gw unless gw.nil? || gw.empty? 155 | else 156 | # The many faces of container networking 157 | 158 | # Host to Container 159 | port22 = network_settings.dig('PortBindings', '22/tcp') 160 | port22 = network_settings.dig('Ports', '22/tcp') if port22.nil? && network_settings.key?('Ports') && !nested_docker? 161 | 162 | ip = port22[0]['HostIp'] if port22 163 | port = port22[0]['HostPort'] if port22 164 | 165 | # Container to container 166 | unless ip && port 167 | ip = network_settings['IPAddress'] 168 | port = (ip && !ip.empty?) ? 22 : nil 169 | end 170 | 171 | # Container through gateway 172 | unless ip && port 173 | ip = network_settings['Gateway'] 174 | 175 | if ip && !ip.empty? 176 | port22 = network_settings.dig('PortBindings', '22/tcp') 177 | port = port22[0]['HostPort'] if port22 178 | else 179 | port = nil 180 | end 181 | end 182 | 183 | # Legacy fallback 184 | unless ip && port 185 | port22 = network_settings.dig('Ports', '22/tcp') 186 | ip = port22[0]['HostIp'] if port22 187 | port = port22[0]['HostPort'] if port22 188 | end 189 | end 190 | 191 | ip = network_settings['IPAddress'] if ip.nil? && host_config['NetworkMode'] != 'slirp4netns' && network_settings['IPAddress'] && !network_settings['IPAddress'].empty? 192 | 193 | if port.nil? 194 | port22 = network_settings.dig('Ports', '22/tcp') 195 | port = port22[0]['HostPort'] if port22 196 | end 197 | 198 | ssh_connection_info[:ip] = (ip == '0.0.0.0') ? '127.0.0.1' : ip 199 | ssh_connection_info[:port] = port || '22' 200 | ssh_connection_info 201 | end 202 | 203 | def provision 204 | @logger.notify 'Provisioning docker' 205 | 206 | @hosts.each do |host| 207 | @logger.notify "provisioning #{host.name}" 208 | 209 | image = get_container_image(host) 210 | 211 | image.tag({ repo: host['tag'] }) if host['tag'] 212 | 213 | if @docker_type == 'swarm' 214 | image_name = "#{@registry}/beaker/#{image.id}" 215 | ret = ::Docker::Image.search(term: image_name) 216 | if ret.first.nil? 217 | @logger.debug('Image does not exist on registry. Pushing.') 218 | image.tag({ repo: image_name, force: true }) 219 | image.push 220 | end 221 | else 222 | image_name = image.id 223 | end 224 | 225 | ### BEGIN CONTAINER OPTIONS MANGLING ### 226 | 227 | container_opts = get_container_opts(host, image_name) 228 | container_opts.merge!(@options[:dockeropts]) if @options[:dockeropts] 229 | container_opts.merge!(host['dockeropts']) if host['dockeropts'] 230 | 231 | container = find_container(host) 232 | 233 | # Provisioning - Only provision if the host's container can't be found 234 | # via its name or ID 235 | if container.nil? 236 | unless host['mount_folders'].nil? 237 | container_opts['HostConfig'] ||= {} 238 | container_opts['HostConfig']['Binds'] = host['mount_folders'].values.map do |mount| 239 | host_path = File.expand_path(mount['host_path']) 240 | # When using docker_toolbox and getting a "(Driveletter):/" path, convert windows path to VM mount 241 | host_path = "/#{host_path.gsub(/^.:/, host_path[/^(.)/].downcase)}" if ENV['DOCKER_TOOLBOX_INSTALL_PATH'] && host_path =~ %r{^.:/} 242 | a = [host_path, mount['container_path']] 243 | 244 | # TODO: rewrite this part 245 | if mount.key?('opts') 246 | a << mount['opts'] if mount.key?('opts') 247 | else 248 | a << mount['opts'] = 'z' 249 | end 250 | 251 | a.join(':') 252 | end 253 | end 254 | 255 | container_opts['Env'] = host['docker_env'] if host['docker_env'] 256 | 257 | # Fixup privileges 258 | # 259 | # If the user has specified CAPs, then we cannot be privileged 260 | # 261 | # If the user has not specified CAPs, we will default to privileged for 262 | # compatibility with worst practice 263 | if host['docker_cap_add'] 264 | container_opts['HostConfig']['CapAdd'] = host['docker_cap_add'] 265 | container_opts['HostConfig'].delete('Privileged') 266 | else 267 | container_opts['HostConfig']['Privileged'] = container_opts['HostConfig']['Privileged'].nil? || container_opts['HostConfig']['Privileged'] 268 | end 269 | 270 | container_opts['name'] = (host['docker_container_name'] || ['beaker', host.name, SecureRandom.uuid.split('-').last].join('-')) 271 | 272 | if host['docker_port_bindings'] 273 | container_opts['ExposedPorts'] = {} if container_opts['ExposedPorts'].nil? 274 | host['docker_port_bindings'].each_pair do |port, bind| 275 | container_opts['ExposedPorts'][port.to_s] = {} 276 | container_opts['HostConfig']['PortBindings'][port.to_s] = bind 277 | end 278 | end 279 | 280 | ### END CONTAINER OPTIONS MANGLING ### 281 | 282 | @logger.debug("Creating container from image #{image_name}") 283 | 284 | ok = false 285 | retries = 0 286 | while !ok && (retries < 5) 287 | container = ::Docker::Container.create(container_opts) 288 | 289 | ssh_info = get_ssh_connection_info(container) 290 | if ::Docker.rootless? && ssh_info[:ip] == '127.0.0.1' && (ssh_info[:port].to_i < 1024) 291 | @logger.debug("#{host} was given a port less than 1024 but you are connecting to a rootless instance, retrying") 292 | 293 | container.delete(force: true) 294 | container = nil 295 | 296 | retries += 1 297 | next 298 | end 299 | 300 | ok = true 301 | end 302 | else 303 | host['use_existing_container'] = true 304 | end 305 | 306 | if container.nil? 307 | raise 'Cannot continue because no existing container ' \ 308 | 'could be found and provisioning is disabled.' 309 | end 310 | 311 | fix_ssh(container) if @options[:provision] == false 312 | 313 | @logger.debug("Starting container #{container.id}") 314 | container.start 315 | 316 | begin 317 | container.stats 318 | rescue StandardError => e 319 | container.delete(force: true) 320 | raise "Container '#{container.id}' in a bad state: #{e}" 321 | end 322 | 323 | # Preserve the ability to talk directly to the underlying API 324 | # 325 | # You can use any method defined by the docker-api gem on this object 326 | # https://github.com/swipely/docker-api 327 | host[:docker_container] = container 328 | 329 | if install_and_run_ssh(host) 330 | @logger.notify("Installing ssh components and starting ssh daemon in #{host} container") 331 | install_ssh_components(container, host) 332 | # run fixssh to configure and start the ssh service 333 | fix_ssh(container, host) 334 | end 335 | 336 | ssh_connection_info = get_ssh_connection_info(container) 337 | 338 | ip = ssh_connection_info[:ip] 339 | port = ssh_connection_info[:port] 340 | 341 | @logger.info("Using container connection at #{ip}:#{port}") 342 | 343 | forward_ssh_agent = @options[:forward_ssh_agent] || false 344 | 345 | host['ip'] = ip 346 | host['port'] = port 347 | host['ssh'] = { 348 | password: root_password, 349 | port: port, 350 | forward_agent: forward_ssh_agent, 351 | auth_methods: %w[password publickey hostbased keyboard-interactive], 352 | } 353 | 354 | @logger.debug("node available as ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@#{ip} -p #{port}") 355 | host['docker_container_id'] = container.id 356 | host['docker_image_id'] = image.id 357 | host['vm_ip'] = container.json['NetworkSettings']['IPAddress'].to_s 358 | 359 | def host.reboot 360 | @logger.warn('Rebooting containers is ineffective...ignoring') 361 | end 362 | end 363 | 364 | hack_etc_hosts @hosts, @options 365 | end 366 | 367 | # This sideloads sshd after a container starts 368 | def install_ssh_components(container, host) 369 | case host['platform'] 370 | when /ubuntu/, /debian/, /cumulus/ 371 | container.exec(%w[apt-get update]) 372 | container.exec(%w[apt-get install -y openssh-server openssh-client]) 373 | container.exec(%w[sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*]) 374 | when /el-[89]/, /fedora-(2[2-9]|3[0-9])/, /amazon-2023/ 375 | container.exec(%w[dnf clean all]) 376 | container.exec(%w[dnf install -y sudo openssh-server openssh-clients]) 377 | container.exec(%w[ssh-keygen -A]) 378 | container.exec(%w[sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*]) 379 | when /^el-/, /centos/, /fedora/, /redhat/, /eos/ 380 | container.exec(%w[yum clean all]) 381 | container.exec(%w[yum install -y sudo openssh-server openssh-clients]) 382 | container.exec(%w[ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key]) 383 | container.exec(%w[ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key]) 384 | container.exec(%w[sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/*]) 385 | when /opensuse/, /sles/ 386 | container.exec(%w[zypper -n in openssh]) 387 | container.exec(%w[ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key]) 388 | container.exec(%w[ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key]) 389 | container.exec(%w[sed -ri 's/^#?UsePAM .*/UsePAM no/' /etc/ssh/sshd_config]) 390 | when /archlinux/ 391 | container.exec(%w[pacman -S --noconfirm openssh]) 392 | container.exec(%w[ssh-keygen -A]) 393 | container.exec(%w[sed -ri 's/^#?UsePAM .*/UsePAM no/' /etc/ssh/sshd_config]) 394 | container.exec(%w[systemctl enable sshd]) 395 | when /alpine/ 396 | container.exec(%w[apk add --update openssh]) 397 | container.exec(%w[ssh-keygen -A]) 398 | else 399 | # TODO: add more platform steps here 400 | raise "platform #{host['platform']} not yet supported on docker" 401 | end 402 | 403 | # Make sshd directory, set root password 404 | container.exec(%w[mkdir -p /var/run/sshd]) 405 | container.exec(['/bin/sh', '-c', "echo root:#{root_password} | chpasswd"]) 406 | end 407 | 408 | def cleanup 409 | @logger.notify 'Cleaning up docker' 410 | @hosts.each do |host| 411 | # leave the container running if docker_preserve_container is set 412 | # setting docker_preserve_container also implies docker_preserve_image 413 | # is set, since you can't delete an image that's the base of a running 414 | # container 415 | next if host['docker_preserve_container'] 416 | 417 | container = find_container(host) 418 | if container 419 | @logger.debug("stop container #{container.id}") 420 | begin 421 | container.kill 422 | sleep 2 # avoid a race condition where the root FS can't unmount 423 | rescue Excon::Errors::ClientError => e 424 | @logger.warn("stop of container #{container.id} failed: #{e.response.body}") 425 | end 426 | @logger.debug("delete container #{container.id}") 427 | begin 428 | container.delete(force: true) 429 | rescue Excon::Errors::ClientError => e 430 | @logger.warn("deletion of container #{container.id} failed: #{e.response.body}") 431 | end 432 | end 433 | 434 | # Do not remove the image if docker_preserve_image is set to true, otherwise remove it 435 | next if host['docker_preserve_image'] 436 | 437 | image_id = host['docker_image_id'] 438 | 439 | if image_id 440 | @logger.debug("deleting image #{image_id}") 441 | begin 442 | ::Docker::Image.remove(image_id) 443 | rescue Excon::Errors::ClientError => e 444 | @logger.warn("deletion of image #{image_id} failed: #{e.response.body}") 445 | rescue ::Docker::Error::DockerError => e 446 | @logger.warn("deletion of image #{image_id} caused internal Docker error: #{e.message}") 447 | end 448 | else 449 | @logger.warn("Intended to delete the host's docker image, but host['docker_image_id'] was not set") 450 | end 451 | end 452 | end 453 | 454 | private 455 | 456 | def root_password 457 | 'root' 458 | end 459 | 460 | def buildargs_for(host) 461 | docker_buildargs = {} 462 | docker_buildargs_env = ENV.fetch('DOCKER_BUILDARGS', nil) 463 | docker_buildargs_env&.split(/ +|\t+/)&.each do |arg| 464 | key, value = arg.split('=') 465 | if key 466 | docker_buildargs[key] = value 467 | else 468 | @logger.warn("DOCKER_BUILDARGS environment variable appears invalid, no key found for value #{value}") 469 | end 470 | end 471 | buildargs = if docker_buildargs.empty? 472 | host['docker_buildargs'] || {} 473 | else 474 | docker_buildargs 475 | end 476 | @logger.debug("Docker build buildargs: #{buildargs}") 477 | JSON.generate(buildargs) 478 | end 479 | 480 | def dockerfile_for(host) 481 | # specify base image 482 | dockerfile = <<~DF 483 | FROM #{host['image']} 484 | ENV container docker 485 | DF 486 | 487 | # Commands before any other commands. Can be used for eg. proxy configuration 488 | dockerfile += (host['docker_image_first_commands'] || []).map { |cmd| "RUN #{cmd}\n" }.join 489 | 490 | # add platform-specific actions 491 | service_name = 'sshd' 492 | additional_packages = host_packages(host) 493 | case host['platform'] 494 | when /ubuntu/, /debian/ 495 | service_name = 'ssh' 496 | dockerfile += <<~DF 497 | RUN apt-get update \ 498 | && apt-get install -y openssh-server openssh-client #{additional_packages.join(' ')} \ 499 | && sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/* 500 | DF 501 | when /cumulus/ 502 | dockerfile += <<~DF 503 | RUN apt-get update \ 504 | && apt-get install -y openssh-server openssh-client #{additional_packages.join(' ')} 505 | DF 506 | when /el-[89]/, /fedora-(2[2-9]|3)/, /amazon-2023/ 507 | dockerfile += <<~DF 508 | RUN dnf clean all \ 509 | && dnf install -y sudo openssh-server openssh-clients #{additional_packages.join(' ')} \ 510 | && ssh-keygen -A \ 511 | && sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/* 512 | DF 513 | when /^el-/, /centos/, /fedora/, /redhat/, /eos/ 514 | dockerfile += <<~DF 515 | RUN yum clean all \ 516 | && yum install -y sudo openssh-server openssh-clients #{additional_packages.join(' ')} \ 517 | && ssh-keygen -A \ 518 | && sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/* 519 | DF 520 | when /opensuse/, /sles/ 521 | dockerfile += <<~DF 522 | RUN zypper -n in openssh #{additional_packages.join(' ')} \ 523 | && ssh-keygen -A \ 524 | && sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/* 525 | DF 526 | when /archlinux/ 527 | dockerfile += <<~DF 528 | RUN pacman --sync --refresh --noconfirm archlinux-keyring \ 529 | && pacman --sync --refresh --noconfirm --sysupgrade \ 530 | && pacman --sync --noconfirm #{additional_packages.join(' ')} \ 531 | && ssh-keygen -A \ 532 | && systemctl enable sshd 533 | DF 534 | else 535 | # TODO: add more platform steps here 536 | raise "platform #{host['platform']} not yet supported on docker" 537 | end 538 | 539 | # Make sshd directory, set root password 540 | dockerfile += <<~DF 541 | RUN mkdir -p /var/run/sshd \ 542 | && echo root:#{root_password} | chpasswd 543 | DF 544 | 545 | # Configure sshd service to allowroot login using password 546 | # Also, disable reverse DNS lookups to prevent every. single. ssh 547 | # operation taking 30 seconds while the lookup times out. 548 | # Also unbreak users with a bunch of SSH keys loaded in their keyring. 549 | # Also unbreak CentOS9 & Fedora containers on Ubuntu 24.04 host (UsePAM no) 550 | dockerfile += <<~DF 551 | RUN sed -ri \ 552 | -e 's/^#?PermitRootLogin .*/PermitRootLogin yes/' \ 553 | -e 's/^#?PasswordAuthentication .*/PasswordAuthentication yes/' \ 554 | -e 's/^#?UseDNS .*/UseDNS no/' \ 555 | -e 's/^#?UsePAM .*/UsePAM no/' \ 556 | -e 's/^#?MaxAuthTries.*/MaxAuthTries 1000/' \ 557 | /etc/ssh/sshd_config $(compgen -G '/etc/ssh/sshd_config.d/*' || echo '') 558 | DF 559 | 560 | # Any extra commands specified for the host 561 | dockerfile += (host['docker_image_commands'] || []).map { |cmd| "RUN #{cmd}\n" }.join 562 | 563 | # Override image entrypoint 564 | dockerfile += "ENTRYPOINT #{host['docker_image_entrypoint']}\n" if host['docker_image_entrypoint'] 565 | 566 | # How to start a sshd on port 22. May be an init for more supervision 567 | # Ensure that the ssh server can be restarted (done from set_env) and container keeps running 568 | cmd = host['docker_cmd'] || ['sh', '-c', "service #{service_name} start; tail -f /dev/null"] 569 | dockerfile += <<~DF 570 | EXPOSE 22 571 | CMD #{cmd} 572 | DF 573 | 574 | @logger.debug("Dockerfile is #{dockerfile}") 575 | 576 | dockerfile 577 | end 578 | 579 | # a puppet run may have changed the ssh config which would 580 | # keep us out of the container. This is a best effort to fix it. 581 | # Optionally pass in a host object to to determine which ssh 582 | # restart command we should try. 583 | def fix_ssh(container, host = nil) 584 | @logger.debug("Fixing ssh on container #{container.id}") 585 | container.exec(['sed', '-ri', 586 | '-e', 's/^#?PermitRootLogin .*/PermitRootLogin yes/', 587 | '-e', 's/^#?PasswordAuthentication .*/PasswordAuthentication yes/', 588 | '-e', 's/^#?UseDNS .*/UseDNS no/', 589 | # Unbreak users with a bunch of SSH keys loaded in their keyring. 590 | '-e', 's/^#?MaxAuthTries.*/MaxAuthTries 1000/', 591 | '/etc/ssh/sshd_config',]) 592 | 593 | return unless host 594 | 595 | case host['platform'] 596 | when /alpine/ 597 | container.exec(%w[/usr/sbin/sshd]) 598 | when /ubuntu/, /debian/ 599 | container.exec(%w[service ssh restart]) 600 | else 601 | container.exec(%w[service sshd restart]) 602 | end 603 | end 604 | 605 | # return the existing container if we're not provisioning 606 | # and docker_container_name is set 607 | def find_container(host) 608 | id = host['docker_container_id'] 609 | name = host['docker_container_name'] 610 | return unless id || name 611 | 612 | containers = ::Docker::Container.all 613 | 614 | if id 615 | @logger.debug("Looking for an existing container with ID #{id}") 616 | container = containers.find { |c| c.id == id } 617 | end 618 | 619 | if name && container.nil? 620 | @logger.debug("Looking for an existing container with name #{name}") 621 | container = containers.find do |c| 622 | c.info['Names'].include? "/#{name}" 623 | end 624 | end 625 | 626 | return container unless container.nil? 627 | 628 | @logger.debug('Existing container not found') 629 | nil 630 | end 631 | 632 | # return true if we are inside a docker container 633 | def in_container? 634 | File.file?('/.dockerenv') 635 | end 636 | end 637 | end 638 | -------------------------------------------------------------------------------- /spec/beaker/hypervisor/docker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'fakefs/spec_helpers' 5 | 6 | module Beaker 7 | platforms = [ 8 | 'ubuntu-14.04-x86_64', 9 | 'fedora-22-x86_64', 10 | 'centos-7-x86_64', 11 | 'sles-12-x86_64', 12 | 'archlinux-2017.12.27-x86_64', 13 | 'amazon-2023-x86_64', 14 | ] 15 | 16 | describe Docker do 17 | require 'docker' 18 | 19 | let(:hosts) do 20 | the_hosts = make_hosts 21 | the_hosts[2]['dockeropts'] = { 22 | Labels: { 23 | 'one' => 3, 24 | 'two' => 4, 25 | }, 26 | } 27 | the_hosts 28 | end 29 | 30 | let(:logger) do 31 | logger = instance_double(Logger) 32 | allow(logger).to receive(:debug) 33 | allow(logger).to receive(:info) 34 | allow(logger).to receive(:warn) 35 | allow(logger).to receive(:error) 36 | allow(logger).to receive(:notify) 37 | logger 38 | end 39 | 40 | let(:options) do 41 | { 42 | logger: logger, 43 | forward_ssh_agent: true, 44 | provision: true, 45 | dockeropts: { 46 | Labels: { 47 | 'one' => 1, 48 | 'two' => 2, 49 | }, 50 | }, 51 | } 52 | end 53 | 54 | let(:image) do 55 | image = instance_double(::Docker::Image) 56 | allow(image).to receive(:id).and_return('zyxwvu') 57 | allow(image).to receive(:tag) 58 | image 59 | end 60 | 61 | let(:container_mode) do 62 | 'rootless' 63 | end 64 | 65 | let(:container_config) do 66 | conf = { 67 | 'HostConfig' => { 68 | 'NetworkMode' => 'slirp4netns', 69 | }, 70 | 'NetworkSettings' => { 71 | 'IPAddress' => '192.0.2.1', 72 | 'Ports' => { 73 | '22/tcp' => [ 74 | { 75 | 'HostIp' => '0.0.0.0', 76 | 'HostPort' => 8022, 77 | }, 78 | ], 79 | }, 80 | 'Gateway' => '192.0.2.254', 81 | }, 82 | } 83 | 84 | conf['HostConfig']['NetworkMode'] = 'bridge' unless container_mode == 'rootless' 85 | 86 | conf 87 | end 88 | 89 | let(:container) do 90 | container = instance_double(::Docker::Container) 91 | allow(container).to receive(:id).and_return('abcdef') 92 | allow(container).to receive(:start) 93 | allow(container).to receive(:stats) 94 | allow(container).to receive(:info).and_return( 95 | *(0..2).map { |index| { 'Names' => ["/spec-container-#{index}"] } }, 96 | ) 97 | allow(container).to receive(:json).and_return(container_config) 98 | allow(container).to receive(:kill) 99 | allow(container).to receive(:delete) 100 | allow(container).to receive(:exec) 101 | container 102 | end 103 | 104 | let(:docker) { ::Beaker::Docker.new(hosts, options) } 105 | 106 | let(:docker_options) { nil } 107 | 108 | let(:version) { { 'ApiVersion' => '1.18', 'Arch' => 'amd64', 'GitCommit' => '4749651', 'GoVersion' => 'go1.4.2', 'KernelVersion' => '3.16.0-37-generic', 'Os' => 'linux', 'Version' => '1.6.0' } } 109 | 110 | before do 111 | allow(::Docker).to receive(:rootless?).and_return(true) 112 | @docker_host = ENV.fetch('DOCKER_HOST', nil) 113 | ENV.delete('DOCKER_HOST') if @docker_host 114 | end 115 | 116 | after do 117 | ENV['DOCKER_HOST'] = @docker_host if @docker_host 118 | end 119 | 120 | context 'with connection failure' do 121 | describe '#initialize' do 122 | before do 123 | require 'excon' 124 | allow(::Docker).to receive(:version).and_raise(Excon::Errors::SocketError.new(StandardError.new('oops'))).exactly(4).times 125 | end 126 | 127 | it 'fails when docker not present' do 128 | expect { docker }.to raise_error(RuntimeError, /Docker instance not connectable/) 129 | expect { docker }.to raise_error(RuntimeError, /Check your DOCKER_HOST variable has been set/) 130 | expect { docker }.to raise_error(RuntimeError, /If you are on OSX or Windows, you might not have Docker Machine setup correctly/) 131 | expect { docker }.to raise_error(RuntimeError, /Error was: oops/) 132 | end 133 | end 134 | end 135 | 136 | context 'with a working connection' do 137 | before do 138 | # Stub out all of the docker-api gem. we should never really call it from these tests 139 | allow(::Docker).to receive(:options).and_return(docker_options) 140 | allow(::Docker).to receive(:podman?).and_return(false) 141 | allow(::Docker).to receive(:version).and_return(version) 142 | allow(::Docker::Image).to receive(:build).and_return(image) 143 | allow(::Docker::Image).to receive(:create).and_return(image) 144 | allow(::Docker::Container).to receive(:create).and_return(container) 145 | end 146 | 147 | describe '#initialize' do 148 | it 'sets Docker options' do 149 | expect(::Docker).to receive(:options=).with({ write_timeout: 300, read_timeout: 300 }).once 150 | 151 | docker 152 | end 153 | 154 | context 'when Docker options are already set' do 155 | let(:docker_options) { { write_timeout: 600, foo: :bar } } 156 | 157 | it 'does not override Docker options' do 158 | expect(::Docker).to receive(:options=).with({ write_timeout: 600, read_timeout: 300, foo: :bar }).once 159 | 160 | docker 161 | end 162 | end 163 | 164 | it 'checks the Docker gem can work with the api' do 165 | expect { docker }.not_to raise_error 166 | end 167 | 168 | it 'hooks the Beaker logger into the Docker one' do 169 | expect(::Docker).to receive(:logger=).with(logger) 170 | 171 | docker 172 | end 173 | end 174 | 175 | describe '#install_ssh_components' do 176 | let(:test_container) { object_double(container) } 177 | let(:host) { hosts[0] } 178 | 179 | before do 180 | allow(docker).to receive(:dockerfile_for) 181 | end 182 | 183 | platforms.each do |platform| 184 | it 'calls exec at least twice' do 185 | host['platform'] = platform 186 | expect(test_container).to receive(:exec).at_least(:twice) 187 | docker.install_ssh_components(test_container, host) 188 | end 189 | end 190 | 191 | it 'accepts alpine as valid platform' do 192 | host['platform'] = Beaker::Platform.new('alpine-3.8-x86_64') 193 | expect(test_container).to receive(:exec).at_least(:twice) 194 | docker.install_ssh_components(test_container, host) 195 | end 196 | 197 | it 'raises an error with an unsupported platform' do 198 | host['platform'] = Beaker::Platform.new('windows-11-64') 199 | expect { docker.install_ssh_components(test_container, host) }.to raise_error(RuntimeError, /windows/) 200 | end 201 | end 202 | 203 | describe '#provision' do 204 | before do 205 | allow(docker).to receive(:dockerfile_for) 206 | end 207 | 208 | context 'when the host has "tag" defined' do 209 | before do 210 | hosts.each do |host| 211 | host['tag'] = 'my_tag' 212 | end 213 | end 214 | 215 | it 'tags the image with the value of the tag' do 216 | expect(image).to receive(:tag).with({ repo: 'my_tag' }).exactly(3).times 217 | docker.provision 218 | end 219 | end 220 | 221 | context 'when the host has "use_image_entry_point" set to true on the host' do 222 | before do 223 | hosts.each do |host| 224 | host['use_image_entry_point'] = true 225 | end 226 | end 227 | 228 | it 'does not call #dockerfile_for but run methods necessary for ssh installation' do 229 | expect(docker).not_to receive(:dockerfile_for) 230 | expect(docker).to receive(:install_ssh_components).exactly(3).times # once per host 231 | expect(docker).to receive(:fix_ssh).exactly(3).times # once per host 232 | docker.provision 233 | end 234 | end 235 | 236 | context 'when the host has a "dockerfile" for the host' do 237 | before do 238 | allow(docker).to receive(:buildargs_for).and_return('buildargs') 239 | hosts.each do |host| 240 | host['dockerfile'] = 'mydockerfile' 241 | end 242 | end 243 | 244 | it 'does not call #dockerfile_for but run methods necessary for ssh installation' do 245 | allow(File).to receive(:exist?).with('mydockerfile').and_return(true) 246 | allow(::Docker::Image).to receive(:build_from_dir).with('/', hash_including(rm: true, buildargs: 'buildargs')).and_return(image) 247 | expect(docker).not_to receive(:dockerfile_for) 248 | expect(docker).to receive(:install_ssh_components).exactly(3).times # once per host 249 | expect(docker).to receive(:fix_ssh).exactly(3).times # once per host 250 | docker.provision 251 | end 252 | end 253 | 254 | it 'calls image create for hosts when use_image_as_is is defined' do 255 | hosts.each do |host| 256 | host['use_image_as_is'] = true 257 | expect(docker).not_to receive(:install_ssh_components) 258 | expect(docker).not_to receive(:fix_ssh) 259 | expect(::Docker::Image).to receive(:create).with('fromImage' => host['image']) # once per host 260 | expect(::Docker::Image).not_to receive(:build) 261 | expect(::Docker::Image).not_to receive(:build_from_dir) 262 | end 263 | 264 | docker.provision 265 | end 266 | 267 | it 'calls dockerfile_for with all the hosts' do 268 | hosts.each do |host| 269 | allow(docker).to receive(:dockerfile_for).with(host).and_return('') 270 | expect(docker).not_to receive(:install_ssh_components) 271 | expect(docker).not_to receive(:fix_ssh) 272 | expect(docker).to receive(:dockerfile_for).with(host) 273 | end 274 | 275 | docker.provision 276 | end 277 | 278 | it 'passes the Dockerfile on to Docker::Image.create' do 279 | allow(docker).to receive(:dockerfile_for).and_return('special testing value') 280 | expect(::Docker::Image).to receive(:build).with('special testing value', { rm: true, buildargs: '{}' }) 281 | 282 | docker.provision 283 | end 284 | 285 | it 'passes the buildargs from ENV DOCKER_BUILDARGS on to Docker::Image.create' do 286 | allow(docker).to receive(:dockerfile_for).and_return('special testing value') 287 | ENV['DOCKER_BUILDARGS'] = 'HTTP_PROXY=http://1.1.1.1:3128' 288 | expect(::Docker::Image).to receive(:build).with('special testing value', { rm: true, buildargs: '{"HTTP_PROXY":"http://1.1.1.1:3128"}' }) 289 | 290 | docker.provision 291 | end 292 | 293 | it 'passes the multiple buildargs from ENV DOCKER_BUILDARGS on to Docker::Image.create' do 294 | allow(docker).to receive(:dockerfile_for).and_return('special testing value') 295 | ENV['DOCKER_BUILDARGS'] = 'HTTP_PROXY=http://1.1.1.1:3128 HTTPS_PROXY=https://1.1.1.1:3129' 296 | expect(::Docker::Image).to receive(:build).with('special testing value', { rm: true, buildargs: '{"HTTP_PROXY":"http://1.1.1.1:3128","HTTPS_PROXY":"https://1.1.1.1:3129"}' }) 297 | 298 | docker.provision 299 | end 300 | 301 | it 'creates a container with correct image and hostname' do 302 | hosts.each_with_index do |host, _index| 303 | expect(::Docker::Container).to receive(:create) do |args| 304 | expect(args[:Image]).to eq(image.id) 305 | expect(args[:Hostname]).to eq(host.name) 306 | end.and_return(container) 307 | end 308 | 309 | docker.provision 310 | end 311 | 312 | it 'creates a container with correct host config settings' do 313 | hosts.each_with_index do |_host, _index| 314 | expect(::Docker::Container).to receive(:create) do |args| 315 | expect(args[:HostConfig][:PublishAllPorts]).to be true 316 | expect(args[:HostConfig][:Privileged]).to be true 317 | expect(args[:HostConfig][:RestartPolicy][:Name]).to eq('always') 318 | end.and_return(container) 319 | end 320 | 321 | docker.provision 322 | end 323 | 324 | it 'creates a container with correct port bindings' do 325 | hosts.each_with_index do |_host, _index| 326 | expect(::Docker::Container).to receive(:create) do |args| 327 | expect(args[:HostConfig][:PortBindings]['22/tcp'][0]['HostPort']).to be_a(String) 328 | expect(args[:HostConfig][:PortBindings]['22/tcp'][0]['HostIp']).to eq('0.0.0.0') 329 | end.and_return(container) 330 | end 331 | 332 | docker.provision 333 | end 334 | 335 | it 'creates a container with correct labels and name' do 336 | hosts.each_with_index do |_host, index| 337 | expect(::Docker::Container).to receive(:create) do |args| 338 | expect(args[:Labels][:one]).to eq(((index == 2) ? 3 : 1)) 339 | expect(args[:Labels][:two]).to eq(((index == 2) ? 4 : 2)) 340 | expect(args[:name]).to match(/\Abeaker-/) 341 | end.and_return(container) 342 | end 343 | 344 | docker.provision 345 | end 346 | 347 | it 'creates a named container with correct basic properties' do 348 | hosts.each_with_index do |host, index| 349 | container_name = "spec-container-#{index}" 350 | host['docker_container_name'] = container_name 351 | 352 | allow(::Docker::Container).to receive(:all).and_return([]) 353 | expect(::Docker::Container).to receive(:create) do |args| 354 | expect(args[:Image]).to eq(image.id) 355 | expect(args[:Hostname]).to eq(host.name) 356 | expect(args[:name]).to eq(container_name) 357 | end.and_return(container) 358 | end 359 | 360 | docker.provision 361 | end 362 | 363 | it 'creates a named container with correct host config' do 364 | hosts.each_with_index do |host, index| 365 | container_name = "spec-container-#{index}" 366 | host['docker_container_name'] = container_name 367 | 368 | allow(::Docker::Container).to receive(:all).and_return([]) 369 | expect(::Docker::Container).to receive(:create) do |args| 370 | expect(args[:HostConfig][:PublishAllPorts]).to be true 371 | expect(args[:HostConfig][:Privileged]).to be true 372 | expect(args[:HostConfig][:RestartPolicy][:Name]).to eq('always') 373 | end.and_return(container) 374 | end 375 | 376 | docker.provision 377 | end 378 | 379 | it 'creates a named container with correct port bindings and labels' do 380 | hosts.each_with_index do |host, index| 381 | container_name = "spec-container-#{index}" 382 | host['docker_container_name'] = container_name 383 | 384 | allow(::Docker::Container).to receive(:all).and_return([]) 385 | expect(::Docker::Container).to receive(:create) do |args| 386 | expect(args[:HostConfig][:PortBindings]['22/tcp'][0]['HostPort']).to be_a(String) 387 | expect(args[:HostConfig][:PortBindings]['22/tcp'][0]['HostIp']).to eq('0.0.0.0') 388 | expect(args[:Labels][:one]).to eq(((index == 2) ? 3 : 1)) 389 | expect(args[:Labels][:two]).to eq(((index == 2) ? 4 : 2)) 390 | end.and_return(container) 391 | end 392 | 393 | docker.provision 394 | end 395 | 396 | it 'creates a container with volumes bound' do 397 | hosts.each_with_index do |host, _index| 398 | host['mount_folders'] = { 399 | 'mount1' => { 400 | 'host_path' => '/source_folder', 401 | 'container_path' => '/mount_point', 402 | }, 403 | 'mount2' => { 404 | 'host_path' => '/another_folder', 405 | 'container_path' => '/another_mount', 406 | 'opts' => 'ro', 407 | }, 408 | 'mount3' => { 409 | 'host_path' => '/different_folder', 410 | 'container_path' => '/different_mount', 411 | 'opts' => 'rw', 412 | }, 413 | 'mount4' => { 414 | 'host_path' => './', 415 | 'container_path' => '/relative_mount', 416 | }, 417 | 'mount5' => { 418 | 'host_path' => 'local_folder', 419 | 'container_path' => '/another_relative_mount', 420 | }, 421 | } 422 | 423 | expect(::Docker::Container).to receive(:create) do |args| 424 | expect(args[:HostConfig][:Binds]).to eq([ 425 | '/source_folder:/mount_point:z', 426 | '/another_folder:/another_mount:ro', 427 | '/different_folder:/different_mount:rw', 428 | "#{File.expand_path('./')}:/relative_mount:z", 429 | "#{File.expand_path('local_folder')}:/another_relative_mount:z", 430 | ]) 431 | end.and_return(container) 432 | end 433 | 434 | docker.provision 435 | end 436 | 437 | it 'creates a volume container with correct image and hostname' do 438 | hosts.each_with_index do |host, _index| 439 | host['mount_folders'] = { 440 | 'mount1' => { 441 | 'host_path' => '/source_folder', 442 | 'container_path' => '/mount_point', 443 | }, 444 | } 445 | 446 | expect(::Docker::Container).to receive(:create) do |args| 447 | expect(args[:Image]).to eq(image.id) 448 | expect(args[:Hostname]).to eq(host.name) 449 | end.and_return(container) 450 | end 451 | 452 | docker.provision 453 | end 454 | 455 | it 'creates a volume container with correct labels and name' do 456 | hosts.each_with_index do |host, index| 457 | host['mount_folders'] = { 458 | 'mount1' => { 459 | 'host_path' => '/source_folder', 460 | 'container_path' => '/mount_point', 461 | }, 462 | } 463 | 464 | expect(::Docker::Container).to receive(:create) do |args| 465 | expect(args[:Labels][:one]).to eq(((index == 2) ? 3 : 1)) 466 | expect(args[:Labels][:two]).to eq(((index == 2) ? 4 : 2)) 467 | expect(args[:name]).to match(/\Abeaker-/) 468 | end.and_return(container) 469 | end 470 | 471 | docker.provision 472 | end 473 | 474 | it 'creates a volume container with correct port bindings' do 475 | hosts.each_with_index do |host, _index| 476 | host['mount_folders'] = { 477 | 'mount1' => { 478 | 'host_path' => '/source_folder', 479 | 'container_path' => '/mount_point', 480 | }, 481 | } 482 | 483 | expect(::Docker::Container).to receive(:create) do |args| 484 | expect(args[:HostConfig][:PortBindings]['22/tcp'][0]['HostPort']).to be_a(String) 485 | expect(args[:HostConfig][:PortBindings]['22/tcp'][0]['HostIp']).to eq('0.0.0.0') 486 | end.and_return(container) 487 | end 488 | 489 | docker.provision 490 | end 491 | 492 | it 'creates a volume container with correct container settings' do 493 | hosts.each_with_index do |host, _index| 494 | host['mount_folders'] = { 495 | 'mount1' => { 496 | 'host_path' => '/source_folder', 497 | 'container_path' => '/mount_point', 498 | }, 499 | } 500 | 501 | expect(::Docker::Container).to receive(:create) do |args| 502 | expect(args[:HostConfig][:PublishAllPorts]).to be true 503 | expect(args[:HostConfig][:Privileged]).to be true 504 | expect(args[:HostConfig][:RestartPolicy][:Name]).to eq('always') 505 | end.and_return(container) 506 | end 507 | 508 | docker.provision 509 | end 510 | 511 | it 'creates a container with capabilities added' do 512 | hosts.each_with_index do |host, _index| 513 | host['docker_cap_add'] = %w[NET_ADMIN SYS_ADMIN] 514 | 515 | expect(::Docker::Container).to receive(:create) do |args| 516 | expect(args[:HostConfig][:CapAdd]).to eq(%w[NET_ADMIN SYS_ADMIN]) 517 | end.and_return(container) 518 | end 519 | 520 | docker.provision 521 | end 522 | 523 | it 'creates a cap container with correct image and hostname' do 524 | hosts.each_with_index do |host, _index| 525 | host['docker_cap_add'] = %w[NET_ADMIN SYS_ADMIN] 526 | 527 | expect(::Docker::Container).to receive(:create) do |args| 528 | expect(args[:Image]).to eq(image.id) 529 | expect(args[:Hostname]).to eq(host.name) 530 | end.and_return(container) 531 | end 532 | 533 | docker.provision 534 | end 535 | 536 | it 'creates a cap container with correct labels and name' do 537 | hosts.each_with_index do |host, index| 538 | host['docker_cap_add'] = %w[NET_ADMIN SYS_ADMIN] 539 | 540 | expect(::Docker::Container).to receive(:create) do |args| 541 | expect(args[:Labels][:one]).to eq(((index == 2) ? 3 : 1)) 542 | expect(args[:Labels][:two]).to eq(((index == 2) ? 4 : 2)) 543 | expect(args[:name]).to match(/\Abeaker-/) 544 | end.and_return(container) 545 | end 546 | 547 | docker.provision 548 | end 549 | 550 | it 'creates a cap container with correct host config' do 551 | hosts.each_with_index do |host, _index| 552 | host['docker_cap_add'] = %w[NET_ADMIN SYS_ADMIN] 553 | 554 | expect(::Docker::Container).to receive(:create) do |args| 555 | expect(args[:HostConfig][:PortBindings]['22/tcp'][0]['HostPort']).to be_a(String) 556 | expect(args[:HostConfig][:PortBindings]['22/tcp'][0]['HostIp']).to eq('0.0.0.0') 557 | expect(args[:HostConfig][:PublishAllPorts]).to be true 558 | expect(args[:HostConfig][:RestartPolicy][:Name]).to eq('always') 559 | end.and_return(container) 560 | end 561 | 562 | docker.provision 563 | end 564 | 565 | it 'creates a container with port bindings' do 566 | hosts.each_with_index do |host, _index| 567 | host['docker_port_bindings'] = { 568 | '8080/tcp' => [{ 'HostPort' => '8080', 'HostIp' => '0.0.0.0' }], 569 | } 570 | 571 | expect(::Docker::Container).to receive(:create) do |args| 572 | expect(args[:ExposedPorts]).to eq({ '8080/tcp' => {} }) 573 | expect(args[:HostConfig][:PortBindings]['8080/tcp']).to eq([{ 'HostPort' => '8080', 'HostIp' => '0.0.0.0' }]) 574 | end.and_return(container) 575 | end 576 | 577 | docker.provision 578 | end 579 | 580 | it 'creates a port binding container with correct image and hostname' do 581 | hosts.each_with_index do |host, _index| 582 | host['docker_port_bindings'] = { 583 | '8080/tcp' => [{ 'HostPort' => '8080', 'HostIp' => '0.0.0.0' }], 584 | } 585 | 586 | expect(::Docker::Container).to receive(:create) do |args| 587 | expect(args[:Image]).to eq(image.id) 588 | expect(args[:Hostname]).to eq(host.name) 589 | end.and_return(container) 590 | end 591 | 592 | docker.provision 593 | end 594 | 595 | it 'creates a port binding container with correct labels and name' do 596 | hosts.each_with_index do |host, index| 597 | host['docker_port_bindings'] = { 598 | '8080/tcp' => [{ 'HostPort' => '8080', 'HostIp' => '0.0.0.0' }], 599 | } 600 | 601 | expect(::Docker::Container).to receive(:create) do |args| 602 | expect(args[:Labels][:one]).to eq(((index == 2) ? 3 : 1)) 603 | expect(args[:Labels][:two]).to eq(((index == 2) ? 4 : 2)) 604 | expect(args[:name]).to match(/\Abeaker-/) 605 | end.and_return(container) 606 | end 607 | 608 | docker.provision 609 | end 610 | 611 | it 'creates a port binding container with correct port bindings' do 612 | hosts.each_with_index do |host, _index| 613 | host['docker_port_bindings'] = { 614 | '8080/tcp' => [{ 'HostPort' => '8080', 'HostIp' => '0.0.0.0' }], 615 | } 616 | 617 | expect(::Docker::Container).to receive(:create) do |args| 618 | expect(args[:HostConfig][:PortBindings]['22/tcp'][0]['HostPort']).to be_a(String) 619 | expect(args[:HostConfig][:PortBindings]['22/tcp'][0]['HostIp']).to eq('0.0.0.0') 620 | end.and_return(container) 621 | end 622 | 623 | docker.provision 624 | end 625 | 626 | it 'creates a port binding container with correct container settings' do 627 | hosts.each_with_index do |host, _index| 628 | host['docker_port_bindings'] = { 629 | '8080/tcp' => [{ 'HostPort' => '8080', 'HostIp' => '0.0.0.0' }], 630 | } 631 | 632 | expect(::Docker::Container).to receive(:create) do |args| 633 | expect(args[:HostConfig][:PublishAllPorts]).to be true 634 | expect(args[:HostConfig][:Privileged]).to be true 635 | expect(args[:HostConfig][:RestartPolicy][:Name]).to eq('always') 636 | end.and_return(container) 637 | end 638 | 639 | docker.provision 640 | end 641 | 642 | it 'starts the container' do 643 | expect(container).to receive(:start) 644 | 645 | docker.provision 646 | end 647 | 648 | context 'when connecting to ssh' do 649 | %w[rootless privileged].each do |mode| 650 | context "when #{mode}" do 651 | let(:container_mode) do 652 | mode 653 | end 654 | 655 | it 'exposes port 22 to beaker' do 656 | docker.provision 657 | 658 | expect(hosts[0]['ip']).to eq '127.0.0.1' 659 | expect(hosts[0]['port']).to eq 8022 660 | end 661 | 662 | it 'exposes port 22 to beaker when using DOCKER_HOST' do 663 | ENV['DOCKER_HOST'] = 'tcp://192.0.2.2:2375' 664 | docker.provision 665 | 666 | expect(hosts[0]['ip']).to eq '192.0.2.2' 667 | expect(hosts[0]['port']).to eq 8022 668 | end 669 | 670 | it 'has ssh agent forwarding enabled' do 671 | docker.provision 672 | 673 | expect(hosts[0]['ip']).to eq '127.0.0.1' 674 | expect(hosts[0]['port']).to eq 8022 675 | expect(hosts[0]['ssh'][:password]).to eq 'root' 676 | expect(hosts[0]['ssh'][:port]).to eq 8022 677 | expect(hosts[0]['ssh'][:forward_agent]).to be true 678 | end 679 | 680 | it 'connects to gateway ip' do 681 | FakeFS do 682 | FileUtils.touch('/.dockerenv') 683 | docker.provision 684 | 685 | expect(hosts[0]['ip']).to eq '192.0.2.254' 686 | expect(hosts[0]['port']).to eq 8022 687 | end 688 | end 689 | end 690 | end 691 | end 692 | 693 | it 'generates a new /etc/hosts file referencing each host' do 694 | ENV['DOCKER_HOST'] = nil 695 | docker.provision 696 | hosts.each do |host| 697 | allow(docker).to receive(:get_domain_name).with(host).and_return('labs.lan') 698 | etc_hosts = <<~HOSTS 699 | 127.0.0.1\tlocalhost localhost.localdomain 700 | 192.0.2.1\tvm1.labs.lan vm1 701 | 192.0.2.1\tvm2.labs.lan vm2 702 | 192.0.2.1\tvm3.labs.lan vm3 703 | HOSTS 704 | expect(docker).to receive(:set_etc_hosts).with(host, etc_hosts).once 705 | end 706 | docker.hack_etc_hosts(hosts, options) 707 | end 708 | 709 | it 'records the image and container for later' do 710 | docker.provision 711 | 712 | expect(hosts[0]['docker_image_id']).to eq image.id 713 | expect(hosts[0]['docker_container_id']).to eq container.id 714 | end 715 | 716 | context 'when provision=false' do 717 | let(:options) do 718 | { 719 | logger: logger, 720 | forward_ssh_agent: true, 721 | provision: false, 722 | } 723 | end 724 | 725 | it 'fixes ssh' do 726 | hosts.each_with_index do |host, index| 727 | container_name = "spec-container-#{index}" 728 | host['docker_container_name'] = container_name 729 | 730 | allow(::Docker::Container).to receive(:all).and_return([container]) 731 | expect(docker).to receive(:fix_ssh).once 732 | end 733 | docker.provision 734 | end 735 | 736 | it 'does not create a container if a named one already exists' do 737 | hosts.each_with_index do |host, index| 738 | container_name = "spec-container-#{index}" 739 | host['docker_container_name'] = container_name 740 | 741 | allow(::Docker::Container).to receive(:all).and_return([container]) 742 | expect(::Docker::Container).not_to receive(:create) 743 | end 744 | 745 | docker.provision 746 | end 747 | end 748 | end 749 | 750 | describe '#cleanup' do 751 | before do 752 | # get into a state where there's something to clean 753 | allow(::Docker::Container).to receive(:all).and_return([container]) 754 | allow(::Docker::Image).to receive(:remove).with(image.id) 755 | allow(docker).to receive(:dockerfile_for) 756 | docker.provision 757 | end 758 | 759 | it 'stops the containers' do 760 | allow(docker).to receive(:sleep).and_return(true) 761 | expect(container).to receive(:kill) 762 | docker.cleanup 763 | end 764 | 765 | it 'deletes the containers' do 766 | allow(docker).to receive(:sleep).and_return(true) 767 | expect(container).to receive(:delete) 768 | docker.cleanup 769 | end 770 | 771 | it 'deletes the images' do 772 | allow(docker).to receive(:sleep).and_return(true) 773 | expect(::Docker::Image).to receive(:remove).with(image.id) 774 | docker.cleanup 775 | end 776 | 777 | it 'does not delete the image if docker_preserve_image is set to true' do 778 | allow(docker).to receive(:sleep).and_return(true) 779 | hosts.each do |host| 780 | host['docker_preserve_image'] = true 781 | end 782 | expect(::Docker::Image).not_to receive(:remove) 783 | docker.cleanup 784 | end 785 | 786 | it 'deletes the image if docker_preserve_image is set to false' do 787 | allow(docker).to receive(:sleep).and_return(true) 788 | hosts.each do |host| 789 | host['docker_preserve_image'] = false 790 | end 791 | expect(::Docker::Image).to receive(:remove).with(image.id) 792 | docker.cleanup 793 | end 794 | end 795 | 796 | describe '#dockerfile_for' do 797 | FakeFS.deactivate! 798 | it 'raises on an unsupported platform' do 799 | expect { docker.send(:dockerfile_for, make_host('none', { 'platform' => 'solaris-11-64', 'image' => 'foobar' })) }.to raise_error(/platform solaris-11-64 not yet supported/) 800 | end 801 | 802 | it 'sets "ENV container docker"' do 803 | FakeFS.deactivate! 804 | platforms.each do |platform| 805 | dockerfile = docker.send(:dockerfile_for, make_host('none', { 806 | 'platform' => platform, 807 | 'image' => 'foobar', 808 | })) 809 | expect(dockerfile).to match(/ENV container docker/) 810 | end 811 | end 812 | 813 | it 'adds docker_image_first_commands as RUN statements' do 814 | FakeFS.deactivate! 815 | platforms.each do |platform| 816 | dockerfile = docker.send(:dockerfile_for, make_host('none', { 817 | 'platform' => platform, 818 | 'image' => 'foobar', 819 | 'docker_image_first_commands' => [ 820 | 'special one', 821 | 'special two', 822 | 'special three', 823 | ], 824 | })) 825 | 826 | expect(dockerfile).to match(/RUN special one\nRUN special two\nRUN special three/) 827 | end 828 | end 829 | 830 | it 'adds docker_image_commands as RUN statements' do 831 | FakeFS.deactivate! 832 | platforms.each do |platform| 833 | dockerfile = docker.send(:dockerfile_for, make_host('none', { 834 | 'platform' => platform, 835 | 'image' => 'foobar', 836 | 'docker_image_commands' => [ 837 | 'special one', 838 | 'special two', 839 | 'special three', 840 | ], 841 | })) 842 | 843 | expect(dockerfile).to match(/RUN special one\nRUN special two\nRUN special three/) 844 | end 845 | end 846 | 847 | it 'adds docker_image_entrypoint' do 848 | FakeFS.deactivate! 849 | platforms.each do |platform| 850 | dockerfile = docker.send(:dockerfile_for, make_host('none', { 851 | 'platform' => platform, 852 | 'image' => 'foobar', 853 | 'docker_image_entrypoint' => '/bin/bash', 854 | })) 855 | 856 | expect(dockerfile).to match(%r{ENTRYPOINT /bin/bash}) 857 | end 858 | end 859 | 860 | it 'uses zypper on sles' do 861 | FakeFS.deactivate! 862 | dockerfile = docker.send(:dockerfile_for, make_host('none', { 863 | 'platform' => Beaker::Platform.new('sles-12-x86_64'), 864 | 'image' => 'foobar', 865 | })) 866 | 867 | expect(dockerfile).to match(/zypper -n in openssh/) 868 | end 869 | 870 | (22..39).to_a.each do |fedora_release| 871 | it "uses dnf on fedora #{fedora_release}" do 872 | FakeFS.deactivate! 873 | dockerfile = docker.send(:dockerfile_for, make_host('none', { 874 | 'platform' => Beaker::Platform.new("fedora-#{fedora_release}-x86_64"), 875 | 'image' => 'foobar', 876 | })) 877 | 878 | expect(dockerfile).to match(/dnf install -y sudo/) 879 | end 880 | end 881 | 882 | it 'uses pacman on archlinux' do 883 | FakeFS.deactivate! 884 | dockerfile = docker.send(:dockerfile_for, make_host('none', { 885 | 'platform' => Beaker::Platform.new('archlinux-current-x86_64'), 886 | 'image' => 'foobar', 887 | })) 888 | 889 | expect(dockerfile).to match(/pacman --sync --refresh --noconfirm archlinux-keyring/) 890 | expect(dockerfile).to match(/pacman --sync --refresh --noconfirm --sysupgrade/) 891 | expect(dockerfile).to match(/pacman --sync --noconfirm curl net-tools openssh/) 892 | end 893 | end 894 | 895 | describe '#fix_ssh' do 896 | let(:test_container) { object_double(container) } 897 | let(:host) { hosts[0] } 898 | 899 | before do 900 | allow(test_container).to receive(:id).and_return('abcdef') 901 | end 902 | 903 | it 'calls exec once when called without host' do 904 | expect(test_container).to receive(:exec).once.with( 905 | include(/PermitRootLogin/) && 906 | include(/PasswordAuthentication/) && 907 | include(/UseDNS/) && 908 | include(/MaxAuthTries/), 909 | ) 910 | docker.send(:fix_ssh, test_container) 911 | end 912 | 913 | it 'execs sshd on alpine' do 914 | host['platform'] = Beaker::Platform.new('alpine-3.8-x86_64') 915 | expect(test_container).to receive(:exec).with(array_including('sed')) 916 | expect(test_container).to receive(:exec).with(%w[/usr/sbin/sshd]) 917 | docker.send(:fix_ssh, test_container, host) 918 | end 919 | 920 | it 'restarts ssh service on ubuntu' do 921 | host['platform'] = Beaker::Platform.new('ubuntu-20.04-x86_64') 922 | expect(test_container).to receive(:exec).with(array_including('sed')) 923 | expect(test_container).to receive(:exec).with(%w[service ssh restart]) 924 | docker.send(:fix_ssh, test_container, host) 925 | end 926 | 927 | it 'restarts sshd service otherwise' do 928 | host['platform'] = Beaker::Platform.new('centos-6-x86_64') 929 | expect(test_container).to receive(:exec).with(array_including('sed')) 930 | expect(test_container).to receive(:exec).with(%w[service sshd restart]) 931 | docker.send(:fix_ssh, test_container, host) 932 | end 933 | end 934 | end 935 | end 936 | end 937 | --------------------------------------------------------------------------------