Class: Kitchen::Docker::Container::Linux

Inherits:
Kitchen::Docker::Container show all
Includes:
Helpers::DockerfileHelper
Defined in:
lib/kitchen/docker/container/linux.rb

Overview

A Linux container, reached over SSH.

The generated image installs and runs an SSH server, and Test Kitchen connects to a published port with a generated key. That is why the Dockerfile here is so much larger than the Windows one.

Constant Summary collapse

MUTEX_FOR_SSH_KEYS =

Serializes SSH key generation across concurrently running instances, which share one key path.

Mutex.new

Constants included from Helpers::ContainerHelper

Helpers::ContainerHelper::COPIED_MARKER

Instance Method Summary collapse

Methods included from Helpers::DockerfileHelper

#almalinux_platform, #amazonlinux_platform, #arch_platform, #centosstream_platform, #debian_platform, #dockerfile_base_linux, #dockerfile_platform, #fedora_platform, #gentoo_paludis_platform, #gentoo_platform, #opensuse_platform, #photonos_platform, #rhel_platform, #rockylinux_platform

Methods inherited from Kitchen::Docker::Container

#destroy, #hostname, #upload

Methods included from Helpers::ImageHelper

#build_image, #image_exists?, #image_in_use?, #parse_image_id, #remove_image

Methods included from Helpers::ContainerHelper

#container_env_variables, #container_exec, #container_exists?, #container_ip_address, #container_running?, #copy_file_to_container, #create_dir_on_container, #dockerfile_path, #dockerfile_proxy_config, #dockerfile_template, #file_on_container?, #ip_address?, #parse_container_id, #proxy_env_vars, #remote_socket?, #remove_container, #replace_env_variables, #run_container, #socket_uri, #verify_file_copied

Methods included from Helpers::CliHelper

#build_copy_command, #build_env_variable_args, #build_exec_command, #build_powershell_command, #build_run_command, #config_to_options, #dev_null, #docker_command, #docker_shell_opts, #docker_sudo_opts, #run_command, #shell_escape

Methods included from Helpers::FileHelper

#create_temp_file

Constructor Details

#initialize(config) ⇒ Linux

Returns a new instance of Linux.

Parameters:

  • config (Hash)

    the driver configuration



38
39
40
# File 'lib/kitchen/docker/container/linux.rb', line 38

def initialize(config)
  super
end

Instance Method Details

#container_ssh_port(state) ⇒ Integer (protected)

The port Test Kitchen should connect to for SSH.

On the internal Docker network the container is reached directly, so the unmapped port 22 is correct; otherwise Docker's published mapping is looked up.

Parameters:

  • state (Hash)

    instance state naming the container

Returns:

  • (Integer)

    the port to connect on

Raises:

  • (Kitchen::ActionFailed)

    if no SSH port is mapped



159
160
161
162
163
164
165
166
# File 'lib/kitchen/docker/container/linux.rb', line 159

def container_ssh_port(state)
  return 22 if @config[:use_internal_docker_network]

  output = docker_command("port #{state[:container_id]} 22/tcp")
  parse_container_ssh_port(output)
rescue => e
  raise ActionFailed, "Docker reports container has no ssh port mapped. #{e}"
end

#create(state) ⇒ void

This method returns an undefined value.

Builds the image, runs the container, and publishes its SSH port.

Parameters:

  • state (Hash)

    mutable instance state; gains ssh_key, image_id, container_id, hostname, and port



47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/kitchen/docker/container/linux.rb', line 47

def create(state)
  super

  debug("Creating Linux container")
  generate_keys

  state[:ssh_key] = @config[:private_key]
  state[:image_id] = build_image(state, dockerfile) unless state[:image_id]
  state[:container_id] = run_container(state, 22) unless state[:container_id]
  state[:hostname] = hostname(state)
  state[:port] = container_ssh_port(state)
end

#dockerfileString (protected)

Builds the Dockerfile for a Linux container.

A configured dockerfile is used as-is after ERB rendering. Otherwise one is generated: the base image, proxy settings, the platform's own package setup, any provision_command entries, and the generated public key appended to the login user's authorized_keys.

Returns:

  • (String)

    the Dockerfile contents



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/kitchen/docker/container/linux.rb', line 176

def dockerfile
  return dockerfile_template if @config[:dockerfile]

  from = "FROM #{@config[:image]}"
  platform = dockerfile_platform
  username = @config[:username]
  public_key = IO.read(@config[:public_key]).strip
  homedir = username == "root" ? "/root" : "/home/#{username}"
  base = dockerfile_base_linux(username, homedir)

  custom = ""
  Array(@config[:provision_command]).each do |cmd|
    custom << "RUN #{cmd}\n"
  end

  ssh_key = "RUN echo #{Shellwords.escape(public_key)} >> #{homedir}/.ssh/authorized_keys"

  # Empty string to ensure the file ends with a newline.
  output = [from, dockerfile_proxy_config, platform, base, custom, ssh_key, ""].join("\n")
  debug("--- Start Dockerfile ---")
  debug(output.strip)
  debug("--- End Dockerfile ---")
  output
end

#execute(command) ⇒ String

Runs a command in the container by uploading it as a shell script.

The command is written to a temp file and executed with bash rather than passed on the command line, which keeps long converge scripts clear of argument-length and quoting limits.

Parameters:

  • command (String)

    the shell code to run

Returns:

  • (String)

    the command's combined output

Raises:

  • (RuntimeError)

    if the command fails



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/kitchen/docker/container/linux.rb', line 69

def execute(command)
  # Create temp script file and upload files to container
  debug("Executing command on Linux container (Platform: #{@config[:platform]})")
  filename = "docker-#{::SecureRandom.uuid}.sh"
  temp_file = "./.kitchen/temp/#{filename}"
  create_temp_file(temp_file, command)

  remote_path = @config[:temp_dir]
  debug("Creating directory #{remote_path} on container")
  create_dir_on_container(@config, remote_path)

  debug("Uploading temp file #{temp_file} to #{remote_path} on container")
  upload(temp_file, remote_path)

  # Replace any environment variables used in the path and execute script file
  debug("Executing temp script #{remote_path}/#{filename} on container")
  remote_path = replace_env_variables(@config, remote_path)

  container_exec(@config, "/bin/bash #{remote_path}/#{filename}")
rescue => e
  raise "Failed to execute command on Linux container. #{e}"
ensure
  # Removed here rather than after the upload, so that a failure part
  # way through does not leave the script behind in .kitchen/temp.
  if temp_file && ::File.exist?(temp_file)
    debug("Deleting temp file from local filesystem")
    ::File.delete(temp_file)
  end
end

#generate_keysvoid (protected)

This method returns an undefined value.

Generates the SSH keypair used to log into Linux containers.

Guarded by MUTEX_FOR_SSH_KEYS because concurrent instances share one key path and would otherwise write the file while another reads it.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/kitchen/docker/container/linux.rb', line 107

def generate_keys
  MUTEX_FOR_SSH_KEYS.synchronize do
    if !File.exist?(@config[:public_key]) || !File.exist?(@config[:private_key])
      private_key = OpenSSL::PKey::RSA.new(2048)
      blobbed_key = Base64.encode64(private_key.to_blob).gsub("\n", "")
      public_key = "ssh-rsa #{blobbed_key} kitchen_docker_key"
      File.open(@config[:private_key], "w") do |file|
        file.write(private_key)
        file.chmod(0600)
      end
      File.open(@config[:public_key], "w") do |file|
        file.write(public_key)
        file.chmod(0600)
      end
    end
  end
end

#parse_container_ssh_port(output) ⇒ Integer (protected)

Pulls the published port out of docker port output.

One line is printed per published binding, and the host part varies:

0.0.0.0:32768
[::]:32768

The port is taken from the end of the line rather than by splitting on ":" from the left, because an IPv6 host contains colons of its own -- splitting left-to-right returned "" for those, and "".to_i is 0, so a daemon publishing on IPv6 handed Test Kitchen port 0 to connect to.

Parameters:

  • output (String)

    e.g. "0.0.0.0:32768\n[::]:32768\n"

Returns:

  • (Integer)

    the host-side port

Raises:

  • (Kitchen::ActionFailed)

    if no port could be found



140
141
142
143
144
145
146
147
148
# File 'lib/kitchen/docker/container/linux.rb', line 140

def parse_container_ssh_port(output)
  port = output.lines.filter_map { |line| line[/:(\d+)\s*\z/, 1] }.first

  if port.nil?
    raise ActionFailed, "Could not parse Docker port output for container SSH port: #{output.inspect}"
  end

  port.to_i
end