mirror of
https://github.com/rapid7/metasploitable3.git
synced 2024-09-13 00:00:48 +02:00
72dc282aa0
Tweaks to the recipes to avoid repetition of work, and ub1404 dev, * let apt cookbook handle apt-update globally * do not download, configure, make, make install if the package is already installed * add guards for file deletion to first check whether file is present * use docker cookbook for image building and running, to only build if not alrady built and only run if not already running * drop mysql table and recreate each time Also, * bump Docker cookbook to 4.9.3 * bump mysql cookbook to 8.5.1 * add apt cookbook for better apt-update management * bump depends versions and add apt * modify readme with customization instructions * modify all chef runlists to call apt first in the runlist * add a vagrantfile for dev of ub1404
764 lines
29 KiB
Ruby
764 lines
29 KiB
Ruby
module DockerCookbook
|
|
class DockerContainer < DockerBase
|
|
resource_name :docker_container
|
|
|
|
property :container_name, String, name_property: true
|
|
property :repo, String, default: lazy { container_name }
|
|
property :tag, String, default: 'latest'
|
|
property :command, [Array, String, nil], coerce: proc { |v| v.is_a?(String) ? ::Shellwords.shellwords(v) : v }
|
|
property :attach_stderr, [TrueClass, FalseClass], default: false, desired_state: false
|
|
property :attach_stdin, [TrueClass, FalseClass], default: false, desired_state: false
|
|
property :attach_stdout, [TrueClass, FalseClass], default: false, desired_state: false
|
|
property :autoremove, [TrueClass, FalseClass], default: false, desired_state: false
|
|
property :cap_add, [Array, nil], coerce: proc { |v| Array(v).empty? ? nil : Array(v) }
|
|
property :cap_drop, [Array, nil], coerce: proc { |v| Array(v).empty? ? nil : Array(v) }
|
|
property :cgroup_parent, String, default: ''
|
|
property :cpu_shares, Integer, default: 0
|
|
property :cpuset_cpus, String, default: ''
|
|
property :detach, [TrueClass, FalseClass], default: true, desired_state: false
|
|
property :devices, Array, default: []
|
|
property :dns, Array, default: []
|
|
property :dns_search, Array, default: []
|
|
property :domain_name, String, default: ''
|
|
property :entrypoint, [Array, String, nil], coerce: proc { |v| v.is_a?(String) ? ::Shellwords.shellwords(v) : v }
|
|
property :env, UnorderedArrayType, default: []
|
|
property :env_file, [Array, String], coerce: proc { |v| coerce_env_file(v) }, default: [], desired_state: false
|
|
property :extra_hosts, [Array, nil], coerce: proc { |v| Array(v).empty? ? nil : Array(v) }
|
|
property :exposed_ports, PartialHashType, default: {}
|
|
property :force, [TrueClass, FalseClass], default: false, desired_state: false
|
|
property :health_check, Hash, default: {}
|
|
property :host, [String, nil], default: lazy { ENV['DOCKER_HOST'] }, desired_state: false
|
|
property :hostname, String
|
|
property :ipc_mode, String, default: ''
|
|
property :kernel_memory, [String, Integer], coerce: proc { |v| coerce_to_bytes(v) }, default: 0
|
|
property :labels, [String, Array, Hash], default: {}, coerce: proc { |v| coerce_labels(v) }
|
|
property :links, UnorderedArrayType, coerce: proc { |v| coerce_links(v) }
|
|
property :log_driver, %w( json-file syslog journald gelf fluentd awslogs splunk etwlogs gcplogs none ), default: 'json-file', desired_state: false
|
|
property :log_opts, [Hash, nil], coerce: proc { |v| coerce_log_opts(v) }, desired_state: false
|
|
property :init, [TrueClass, FalseClass, nil]
|
|
property :ip_address, String
|
|
property :mac_address, String
|
|
property :memory, [String, Integer], coerce: proc { |v| coerce_to_bytes(v) }, default: 0
|
|
property :memory_swap, [String, Integer], coerce: proc { |v| coerce_to_bytes(v) }, default: 0
|
|
property :memory_swappiness, Integer, default: 0
|
|
property :memory_reservation, Integer, coerce: proc { |v| coerce_to_bytes(v) }, default: 0
|
|
property :network_disabled, [TrueClass, FalseClass], default: false
|
|
property :network_mode, String, default: 'bridge'
|
|
property :network_aliases, [String, Array], default: [], coerce: proc { |v| Array(v) }
|
|
property :oom_kill_disable, [TrueClass, FalseClass], default: false
|
|
property :oom_score_adj, Integer, default: -500
|
|
property :open_stdin, [TrueClass, FalseClass], default: false, desired_state: false
|
|
property :outfile, String
|
|
property :port_bindings, PartialHashType, default: {}
|
|
property :pid_mode, String, default: ''
|
|
property :privileged, [TrueClass, FalseClass], default: false
|
|
property :publish_all_ports, [TrueClass, FalseClass], default: false
|
|
property :remove_volumes, [TrueClass, FalseClass], default: false
|
|
property :restart_maximum_retry_count, Integer, default: 0
|
|
property :restart_policy, String
|
|
property :runtime, String, default: 'runc'
|
|
property :ro_rootfs, [TrueClass, FalseClass], default: false
|
|
property :security_opt, [String, Array], coerce: proc { |v| v.nil? ? nil : Array(v) }
|
|
property :shm_size, [String, Integer], default: '64m', coerce: proc { |v| coerce_to_bytes(v) }
|
|
property :signal, String, default: 'SIGTERM'
|
|
property :stdin_once, [TrueClass, FalseClass], default: false, desired_state: false
|
|
property :sysctls, Hash, default: {}
|
|
property :timeout, Integer, desired_state: false
|
|
property :tty, [TrueClass, FalseClass], default: false
|
|
property :ulimits, [Array, nil], coerce: proc { |v| coerce_ulimits(v) }
|
|
property :user, String, default: ''
|
|
property :userns_mode, String, default: ''
|
|
property :uts_mode, String, default: ''
|
|
property :volumes, PartialHashType, default: {}, coerce: proc { |v| coerce_volumes(v) }
|
|
property :volumes_from, [String, Array], coerce: proc { |v| v.nil? ? nil : Array(v) }
|
|
property :volume_driver, String
|
|
property :working_dir, String, default: ''
|
|
|
|
# Used to store the bind property since binds is an alias to volumes
|
|
property :volumes_binds, Array
|
|
|
|
# Used to store the state of the Docker container
|
|
property :container, Docker::Container, desired_state: false
|
|
|
|
# Used to store the state of the Docker container create options
|
|
property :create_options, Hash, default: {}, desired_state: false
|
|
|
|
# Used by :stop action. If the container takes longer than this
|
|
# many seconds to stop, kill it instead. A nil value (the default) means
|
|
# never kill the container.
|
|
property :kill_after, [Integer, NilClass], default: nil, desired_state: false
|
|
|
|
alias_method :cmd, :command
|
|
alias_method :additional_host, :extra_hosts
|
|
alias_method :rm, :autoremove
|
|
alias_method :remove_automatically, :autoremove
|
|
alias_method :host_name, :hostname
|
|
alias_method :domainname, :domain_name
|
|
alias_method :dnssearch, :dns_search
|
|
alias_method :restart_maximum_retries, :restart_maximum_retry_count
|
|
alias_method :volume, :volumes
|
|
alias_method :binds, :volumes
|
|
alias_method :volume_from, :volumes_from
|
|
alias_method :destination, :outfile
|
|
alias_method :workdir, :working_dir
|
|
|
|
###################
|
|
# Property helpers
|
|
###################
|
|
|
|
def coerce_labels(v)
|
|
case v
|
|
when Hash, nil
|
|
v
|
|
else
|
|
Array(v).each_with_object({}) do |label, h|
|
|
parts = label.split(':')
|
|
h[parts[0]] = parts[1..-1].join(':')
|
|
end
|
|
end
|
|
end
|
|
|
|
def coerce_links(v)
|
|
case v
|
|
when DockerBase::UnorderedArray, nil
|
|
v
|
|
else
|
|
return nil if v.empty?
|
|
# Parse docker input of /source:/container_name/dest into source:dest
|
|
DockerBase::UnorderedArray.new(Array(v)).map! do |link|
|
|
if link =~ %r{^/(?<source>.+):/#{name}/(?<dest>.+)}
|
|
link = "#{Regexp.last_match[:source]}:#{Regexp.last_match[:dest]}"
|
|
end
|
|
link
|
|
end
|
|
end
|
|
end
|
|
|
|
def to_bytes(v)
|
|
n = v.to_i
|
|
u = v.gsub(/\d/, '').upcase
|
|
|
|
multiplier = case u
|
|
when 'B'
|
|
1
|
|
when 'K'
|
|
1024**1
|
|
when 'M'
|
|
1024**2
|
|
when 'G'
|
|
1024**3
|
|
when 'T'
|
|
1024**4
|
|
when 'P'
|
|
1024**5
|
|
when 'E'
|
|
1024**6
|
|
when 'Z'
|
|
1024**7
|
|
when 'Y'
|
|
1024**8
|
|
else
|
|
1
|
|
end
|
|
|
|
n * multiplier
|
|
end
|
|
|
|
def coerce_to_bytes(v)
|
|
case v
|
|
when Integer, nil
|
|
v
|
|
else
|
|
to_bytes(v)
|
|
end
|
|
end
|
|
|
|
def coerce_log_opts(v)
|
|
case v
|
|
when Hash, nil
|
|
v
|
|
else
|
|
Array(v).each_with_object({}) do |log_opt, memo|
|
|
key, value = log_opt.split('=', 2)
|
|
memo[key] = value
|
|
end
|
|
end
|
|
end
|
|
|
|
def coerce_ulimits(v)
|
|
return v if v.nil?
|
|
Array(v).map do |u|
|
|
u = "#{u['Name']}=#{u['Soft']}:#{u['Hard']}" if u.is_a?(Hash)
|
|
u
|
|
end
|
|
end
|
|
|
|
def coerce_volumes(v)
|
|
case v
|
|
when DockerBase::PartialHash, nil
|
|
v
|
|
when Hash
|
|
DockerBase::PartialHash[v]
|
|
else
|
|
b = []
|
|
v = Array(v).to_a # in case v.is_A?(Chef::Node::ImmutableArray)
|
|
v.delete_if do |x|
|
|
parts = x.split(':')
|
|
b << x if parts.length > 1
|
|
end
|
|
b = nil if b.empty?
|
|
volumes_binds b
|
|
return DockerBase::PartialHash.new if v.empty?
|
|
v.each_with_object(DockerBase::PartialHash.new) { |volume, h| h[volume] = {} }
|
|
end
|
|
end
|
|
|
|
def state
|
|
# Always return the latest state, see #510
|
|
Docker::Container.get(container_name, {}, connection).info['State']
|
|
rescue StandardError
|
|
{}
|
|
end
|
|
|
|
def wait_running_state(v)
|
|
tries = running_wait_time
|
|
tries.times do
|
|
return if state['Running'] == v
|
|
sleep 1
|
|
end
|
|
return if state['Running'] == v
|
|
|
|
# Container failed to reach correct state: Throw an error
|
|
desired_state_str = v ? 'running' : 'not running'
|
|
raise Docker::Error::TimeoutError, "Container #{container_name} failed to change to #{desired_state_str} state after #{tries} seconds"
|
|
end
|
|
|
|
def port(v = nil)
|
|
return @port if v.nil?
|
|
exposed_ports coerce_exposed_ports(v)
|
|
port_bindings coerce_port_bindings(v)
|
|
@port = v
|
|
@port
|
|
end
|
|
|
|
def parse_port(v)
|
|
_, protocol = v.split('/')
|
|
parts = v.split(':')
|
|
case parts.length
|
|
when 3
|
|
host_ip = parts[0]
|
|
host_port = parts[1].split('-')
|
|
container_port = parts[2].split('-')
|
|
when 2
|
|
host_ip = '0.0.0.0'
|
|
host_port = parts[0].split('-')
|
|
container_port = parts[1].split('-')
|
|
when 1
|
|
host_ip = ''
|
|
host_port = ['']
|
|
container_port = parts[0].split('-')
|
|
end
|
|
host_port.map!(&:to_i) unless host_port == ['']
|
|
container_port.map!(&:to_i)
|
|
if host_port.count > 1
|
|
Chef::Log.fatal("FATAL: Invalid port range! #{host_port}") if host_port[0] > host_port[1]
|
|
host_port = (host_port[0]..host_port[1]).to_a
|
|
end
|
|
if container_port.count > 1
|
|
Chef::Log.fatal("FATAL: Invalid port range! #{container_port}") if container_port[0] > container_port[1]
|
|
container_port = (container_port[0]..container_port[1]).to_a
|
|
end
|
|
Chef::Log.fatal('FATAL: Port range size does not match!') if host_port.count > 1 && host_port.count != container_port.count
|
|
# qualify the port-binding protocol even when it is implicitly tcp #427.
|
|
protocol = 'tcp' if protocol.nil?
|
|
Array(container_port).map.with_index do |_, i|
|
|
{
|
|
'host_ip' => host_ip,
|
|
'host_port' => host_port[i].to_s,
|
|
'container_port' => "#{container_port[i]}/#{protocol}",
|
|
}
|
|
end
|
|
end
|
|
|
|
def coerce_exposed_ports(v)
|
|
case v
|
|
when Hash, nil
|
|
v
|
|
else
|
|
x = Array(v).map { |a| parse_port(a) }
|
|
x.flatten!
|
|
x.each_with_object({}) do |y, h|
|
|
h[y['container_port']] = {}
|
|
end
|
|
end
|
|
end
|
|
|
|
def coerce_port_bindings(v)
|
|
case v
|
|
when Hash, nil
|
|
v
|
|
else
|
|
x = Array(v).map { |a| parse_port(a) }
|
|
x.flatten!
|
|
x.each_with_object({}) do |y, h|
|
|
h[y['container_port']] = [] unless h[y['container_port']]
|
|
h[y['container_port']] << {
|
|
'HostIp' => y['host_ip'],
|
|
'HostPort' => y['host_port'],
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
def coerce_env_file(v)
|
|
return v if v.empty?
|
|
Array(v).map { |f| ::File.readlines(f).map(&:strip) }.flatten
|
|
end
|
|
|
|
# log_driver and log_opts really handle this
|
|
def log_config(value = Chef::NOT_PASSED)
|
|
if value != Chef::NOT_PASSED
|
|
@log_config = value
|
|
log_driver value['Type']
|
|
log_opts value['Config']
|
|
end
|
|
return @log_config if defined?(@log_config)
|
|
def_logcfg = {}
|
|
def_logcfg['Type'] = log_driver if property_is_set?(:log_driver)
|
|
def_logcfg['Config'] = log_opts if property_is_set?(:log_opts)
|
|
def_logcfg = nil if def_logcfg.empty?
|
|
def_logcfg
|
|
end
|
|
|
|
# TODO: test image property in serverspec and kitchen, not only in rspec
|
|
# for full specs of image parsing, see spec/helpers_container_spec.rb
|
|
#
|
|
# If you say: `repo 'blah'`
|
|
# Image will be: `blah:latest`
|
|
#
|
|
# If you say: `repo 'blah'; tag '3.1'`
|
|
# Image will be: `blah:3.1`
|
|
#
|
|
# If you say: `image 'blah'`
|
|
# Repo will be: `blah`
|
|
# Tag will be: `latest`
|
|
#
|
|
# If you say: `image 'blah:3.1'`
|
|
# Repo will be: `blah`
|
|
# Tag will be: `3.1`
|
|
#
|
|
# If you say: `image 'repo/blah'`
|
|
# Repo will be: `repo/blah`
|
|
# Tag will be: `latest`
|
|
#
|
|
# If you say: `image 'repo/blah:3.1'`
|
|
# Repo will be: `repo/blah`
|
|
# Tag will be: `3.1`
|
|
#
|
|
# If you say: `image 'repo:1337/blah'`
|
|
# Repo will be: `repo:1337/blah`
|
|
# Tag will be: `latest'
|
|
#
|
|
# If you say: `image 'repo:1337/blah:3.1'`
|
|
# Repo will be: `repo:1337/blah`
|
|
# Tag will be: `3.1`
|
|
#
|
|
def image(image = nil)
|
|
if image
|
|
if image.include?('/')
|
|
# pathological case, a ':' may be present which starts the 'port'
|
|
# part of the image name and not a tag. example: 'host:1337/blah'
|
|
# fortunately, tags are only found in the 'basename' part of image
|
|
# so we can split on '/' and rebuild once the tag has been parsed.
|
|
dirname, _, basename = image.rpartition('/')
|
|
r, t = basename.split(':', 2)
|
|
r = [dirname, r].join('/')
|
|
else
|
|
# normal case, the ':' starts the tag part
|
|
r, t = image.split(':', 2)
|
|
end
|
|
repo r
|
|
tag t if t
|
|
end
|
|
"#{repo}:#{tag}"
|
|
end
|
|
|
|
def to_shellwords(command)
|
|
command.is_a?(String) ? ::Shellwords.shellwords(command) : command
|
|
end
|
|
|
|
######################
|
|
# Load Current Value
|
|
######################
|
|
|
|
def to_snake_case(name)
|
|
# ExposedPorts -> _exposed_ports
|
|
name = name.gsub(/[A-Z]/) { |x| "_#{x.downcase}" }
|
|
# _exposed_ports -> exposed_ports
|
|
name = name[1..-1] if name.start_with?('_')
|
|
name
|
|
end
|
|
|
|
load_current_value do
|
|
# Grab the container and assign the container property
|
|
begin
|
|
with_retries { container Docker::Container.get(container_name, {}, connection) }
|
|
rescue Docker::Error::NotFoundError
|
|
current_value_does_not_exist!
|
|
end
|
|
|
|
# Go through everything in the container and set corresponding properties:
|
|
# c.info['Config']['ExposedPorts'] -> exposed_ports
|
|
(container.info['Config'].to_a + container.info['HostConfig'].to_a).each do |key, value|
|
|
next if value.nil? || key == 'RestartPolicy' || key == 'Binds' || key == 'ReadonlyRootfs'
|
|
|
|
# Image => image
|
|
# Set exposed_ports = ExposedPorts (etc.)
|
|
property_name = to_snake_case(key)
|
|
public_send(property_name, value) if respond_to?(property_name)
|
|
end
|
|
|
|
# load container specific labels (without engine/image ones)
|
|
load_container_labels
|
|
|
|
# these are a special case for us because our names differ from theirs
|
|
restart_policy container.info['HostConfig']['RestartPolicy']['Name']
|
|
restart_maximum_retry_count container.info['HostConfig']['RestartPolicy']['MaximumRetryCount']
|
|
volumes_binds container.info['HostConfig']['Binds']
|
|
ro_rootfs container.info['HostConfig']['ReadonlyRootfs']
|
|
ip_address ip_address_from_container_networks(container) unless ip_address_from_container_networks(container).nil?
|
|
end
|
|
|
|
# Gets the ip address from the existing container
|
|
# current docker api of 1.16 does not have ['NetworkSettings']['Networks']
|
|
# For docker > 1.21 - use ['NetworkSettings']['Networks']
|
|
#
|
|
# @param container [Docker::Container] A container object
|
|
# @returns [String] An ip_address
|
|
def ip_address_from_container_networks(container)
|
|
# We use the first value in 'Networks'
|
|
# We can't assume it will be 'bridged'
|
|
# It might also not match the new_resource value
|
|
if container.info['NetworkSettings'] &&
|
|
container.info['NetworkSettings']['Networks'] &&
|
|
container.info['NetworkSettings']['Networks'].values[0] &&
|
|
container.info['NetworkSettings']['Networks'].values[0]['IPAMConfig'] &&
|
|
container.info['NetworkSettings']['Networks'].values[0]['IPAMConfig']['IPv4Address']
|
|
# Return the ip address listed
|
|
container.info['NetworkSettings']['Networks'].values[0]['IPAMConfig']['IPv4Address']
|
|
end
|
|
end
|
|
|
|
#########
|
|
# Actions
|
|
#########
|
|
|
|
# Super handy visual reference!
|
|
# http://gliderlabs.com/images/docker_events.png
|
|
|
|
# Loads container specific labels excluding those of engine or image.
|
|
# This insures idempotency.
|
|
def load_container_labels
|
|
image_labels = Docker::Image.get(container.info['Image'], {}, connection).info['Config']['Labels'] || {}
|
|
engine_labels = Docker.info(connection)['Labels'] || {}
|
|
|
|
labels = (container.info['Config']['Labels'] || {}).reject do |key, val|
|
|
image_labels.any? { |k, v| k == key && v == val } ||
|
|
engine_labels.any? { |k, v| k == key && v == val }
|
|
end
|
|
|
|
public_send(:labels, labels)
|
|
end
|
|
|
|
action :run do
|
|
validate_container_create
|
|
call_action(:create)
|
|
call_action(:start)
|
|
call_action(:delete) if new_resource.autoremove
|
|
end
|
|
|
|
action :create do
|
|
validate_container_create
|
|
|
|
converge_if_changed do
|
|
action_delete
|
|
|
|
with_retries do
|
|
config = {
|
|
'name' => new_resource.container_name,
|
|
'Image' => "#{new_resource.repo}:#{new_resource.tag}",
|
|
'Labels' => new_resource.labels,
|
|
'Cmd' => to_shellwords(new_resource.command),
|
|
'AttachStderr' => new_resource.attach_stderr,
|
|
'AttachStdin' => new_resource.attach_stdin,
|
|
'AttachStdout' => new_resource.attach_stdout,
|
|
'Domainname' => new_resource.domain_name,
|
|
'Entrypoint' => to_shellwords(new_resource.entrypoint),
|
|
'Env' => new_resource.env + new_resource.env_file,
|
|
'ExposedPorts' => new_resource.exposed_ports,
|
|
'Hostname' => parsed_hostname,
|
|
'MacAddress' => new_resource.mac_address,
|
|
'NetworkDisabled' => new_resource.network_disabled,
|
|
'OpenStdin' => new_resource.open_stdin,
|
|
'StdinOnce' => new_resource.stdin_once,
|
|
'Tty' => new_resource.tty,
|
|
'User' => new_resource.user,
|
|
'Volumes' => new_resource.volumes,
|
|
'WorkingDir' => new_resource.working_dir,
|
|
'HostConfig' => {
|
|
'Binds' => new_resource.volumes_binds,
|
|
'CapAdd' => new_resource.cap_add,
|
|
'CapDrop' => new_resource.cap_drop,
|
|
'CgroupParent' => new_resource.cgroup_parent,
|
|
'CpuShares' => new_resource.cpu_shares,
|
|
'CpusetCpus' => new_resource.cpuset_cpus,
|
|
'Devices' => new_resource.devices,
|
|
'Dns' => new_resource.dns,
|
|
'DnsSearch' => new_resource.dns_search,
|
|
'ExtraHosts' => new_resource.extra_hosts,
|
|
'IpcMode' => new_resource.ipc_mode,
|
|
'Init' => new_resource.init,
|
|
'KernelMemory' => new_resource.kernel_memory,
|
|
'Links' => new_resource.links,
|
|
'LogConfig' => log_config,
|
|
'Memory' => new_resource.memory,
|
|
'MemorySwap' => new_resource.memory_swap,
|
|
'MemorySwappiness' => new_resource.memory_swappiness,
|
|
'MemoryReservation' => new_resource.memory_reservation,
|
|
'NetworkMode' => new_resource.network_mode,
|
|
'OomKillDisable' => new_resource.oom_kill_disable,
|
|
'OomScoreAdj' => new_resource.oom_score_adj,
|
|
'Privileged' => new_resource.privileged,
|
|
'PidMode' => new_resource.pid_mode,
|
|
'PortBindings' => new_resource.port_bindings,
|
|
'PublishAllPorts' => new_resource.publish_all_ports,
|
|
'RestartPolicy' => {
|
|
'Name' => new_resource.restart_policy,
|
|
'MaximumRetryCount' => new_resource.restart_maximum_retry_count,
|
|
},
|
|
'ReadonlyRootfs' => new_resource.ro_rootfs,
|
|
'Runtime' => new_resource.runtime,
|
|
'SecurityOpt' => new_resource.security_opt,
|
|
'ShmSize' => new_resource.shm_size,
|
|
'Sysctls' => new_resource.sysctls,
|
|
'Ulimits' => ulimits_to_hash,
|
|
'UsernsMode' => new_resource.userns_mode,
|
|
'UTSMode' => new_resource.uts_mode,
|
|
'VolumesFrom' => new_resource.volumes_from,
|
|
'VolumeDriver' => new_resource.volume_driver,
|
|
},
|
|
}
|
|
net_config = {
|
|
'NetworkingConfig' => {
|
|
'EndpointsConfig' => {
|
|
new_resource.network_mode => {
|
|
'IPAMConfig' => {
|
|
'IPv4Address' => new_resource.ip_address,
|
|
},
|
|
'Aliases' => new_resource.network_aliases,
|
|
},
|
|
},
|
|
},
|
|
} if new_resource.network_mode
|
|
config.merge! net_config
|
|
|
|
# Remove any options not supported in windows
|
|
if platform?('windows')
|
|
config['HostConfig'].delete('MemorySwappiness')
|
|
end
|
|
|
|
unless new_resource.health_check.empty?
|
|
config['Healthcheck'] = new_resource.health_check
|
|
end
|
|
|
|
# Store the state of the options and create the container
|
|
new_resource.create_options = config
|
|
Docker::Container.create(config, connection)
|
|
end
|
|
end
|
|
end
|
|
|
|
action :start do
|
|
return if state['Restarting']
|
|
return if state['Running']
|
|
converge_by "starting #{new_resource.container_name}" do
|
|
with_retries do
|
|
current_resource.container.start
|
|
|
|
unless new_resource.detach
|
|
new_resource.timeout ? current_resource.container.wait(new_resource.timeout) : current_resource.container.wait
|
|
end
|
|
end
|
|
wait_running_state(true) if new_resource.detach
|
|
end
|
|
end
|
|
|
|
action :stop do
|
|
return unless state['Running']
|
|
kill_after_str = "(will kill after #{new_resource.kill_after}s)" if new_resource.kill_after
|
|
converge_by "stopping #{new_resource.container_name} #{kill_after_str}" do
|
|
begin
|
|
with_retries do
|
|
current_resource.container.stop!('timeout' => new_resource.kill_after)
|
|
wait_running_state(false)
|
|
end
|
|
rescue Docker::Error::TimeoutError
|
|
raise Docker::Error::TimeoutError, "Container failed to stop, consider adding kill_after to the container #{new_resource.container_name}"
|
|
end
|
|
end
|
|
end
|
|
|
|
action :kill do
|
|
return unless state['Running']
|
|
converge_by "killing #{new_resource.container_name}" do
|
|
with_retries { current_resource.container.kill(signal: new_resource.signal) }
|
|
end
|
|
end
|
|
|
|
action :run_if_missing do
|
|
return if current_resource
|
|
call_action(:run)
|
|
end
|
|
|
|
action :pause do
|
|
return if state['Paused']
|
|
converge_by "pausing #{new_resource.container_name}" do
|
|
with_retries { current_resource.container.pause }
|
|
end
|
|
end
|
|
|
|
action :unpause do
|
|
return if current_resource && !state['Paused']
|
|
converge_by "unpausing #{new_resource.container_name}" do
|
|
with_retries { current_resource.container.unpause }
|
|
end
|
|
end
|
|
|
|
action :restart do
|
|
kill_after_str = " (will kill after #{new_resource.kill_after}s)" if new_resource.kill_after != -1
|
|
converge_by "restarting #{new_resource.container_name} #{kill_after_str}" do
|
|
current_resource ? current_resource.container.restart('timeout' => new_resource.kill_after) : call_action(:run)
|
|
end
|
|
end
|
|
|
|
action :reload do
|
|
converge_by "reloading #{new_resource.container_name}" do
|
|
with_retries { current_resource.container.kill(signal: 'SIGHUP') }
|
|
end
|
|
end
|
|
|
|
action :redeploy do
|
|
validate_container_create
|
|
|
|
# never start containers resulting from a previous action :create #432
|
|
should_create = state['Running'] == false && state['StartedAt'] == '0001-01-01T00:00:00Z'
|
|
call_action(:delete)
|
|
call_action(should_create ? :create : :run)
|
|
end
|
|
|
|
action :delete do
|
|
return unless current_resource
|
|
call_action(:unpause)
|
|
call_action(:stop)
|
|
converge_by "deleting #{new_resource.container_name}" do
|
|
with_retries { current_resource.container.delete(force: new_resource.force, v: new_resource.remove_volumes) }
|
|
end
|
|
end
|
|
|
|
action :remove do
|
|
call_action(:delete)
|
|
end
|
|
|
|
action :commit do
|
|
converge_by "committing #{new_resource.container_name}" do
|
|
with_retries do
|
|
new_image = current_resource.container.commit
|
|
new_image.tag('repo' => new_resource.repo, 'tag' => new_resource.tag, 'force' => new_resource.force)
|
|
end
|
|
end
|
|
end
|
|
|
|
action :export do
|
|
raise "Please set outfile property on #{new_resource.container_name}" if new_resource.outfile.nil?
|
|
converge_by "exporting #{new_resource.container_name}" do
|
|
with_retries do
|
|
::File.open(new_resource.outfile, 'w') { |f| current_resource.container.export { |chunk| f.write(chunk) } }
|
|
end
|
|
end
|
|
end
|
|
|
|
declare_action_class.class_eval do
|
|
def validate_container_create
|
|
if new_resource.property_is_set?(:restart_policy) &&
|
|
new_resource.restart_policy != 'no' &&
|
|
new_resource.restart_policy != 'always' &&
|
|
new_resource.restart_policy != 'unless-stopped' &&
|
|
new_resource.restart_policy != 'on-failure'
|
|
raise Chef::Exceptions::ValidationFailed, 'restart_policy must be either no, always, unless-stopped, or on-failure.'
|
|
end
|
|
|
|
if new_resource.autoremove == true && (new_resource.property_is_set?(:restart_policy) && restart_policy != 'no')
|
|
raise Chef::Exceptions::ValidationFailed, 'Conflicting options restart_policy and autoremove.'
|
|
end
|
|
|
|
if new_resource.detach == true &&
|
|
(
|
|
new_resource.attach_stderr == true ||
|
|
new_resource.attach_stdin == true ||
|
|
new_resource.attach_stdout == true ||
|
|
new_resource.stdin_once == true
|
|
)
|
|
raise Chef::Exceptions::ValidationFailed, 'Conflicting options detach, attach_stderr, attach_stdin, attach_stdout, stdin_once.'
|
|
end
|
|
|
|
if new_resource.network_mode == 'host' &&
|
|
(
|
|
!(new_resource.hostname.nil? || new_resource.hostname.empty?) ||
|
|
!(new_resource.mac_address.nil? || new_resource.mac_address.empty?)
|
|
)
|
|
raise Chef::Exceptions::ValidationFailed, 'Cannot specify hostname or mac_address when network_mode is host.'
|
|
end
|
|
|
|
if new_resource.network_mode == 'container' &&
|
|
(
|
|
!(new_resource.hostname.nil? || new_resource.hostname.empty?) ||
|
|
!(new_resource.dns.nil? || new_resource.dns.empty?) ||
|
|
!(new_resource.dns_search.nil? || new_resource.dns_search.empty?) ||
|
|
!(new_resource.mac_address.nil? || new_resource.mac_address.empty?) ||
|
|
!(new_resource.extra_hosts.nil? || new_resource.extra_hosts.empty?) ||
|
|
!(new_resource.exposed_ports.nil? || new_resource.exposed_ports.empty?) ||
|
|
!(new_resource.port_bindings.nil? || new_resource.port_bindings.empty?) ||
|
|
!(new_resource.publish_all_ports.nil? || new_resource.publish_all_ports.empty?) ||
|
|
!new_resource.port.nil?
|
|
)
|
|
raise Chef::Exceptions::ValidationFailed, 'Cannot specify hostname, dns, dns_search, mac_address, extra_hosts, exposed_ports, port_bindings, publish_all_ports, port when network_mode is container.'
|
|
end
|
|
end
|
|
|
|
def parsed_hostname
|
|
return nil if new_resource.network_mode == 'host'
|
|
new_resource.hostname
|
|
end
|
|
|
|
def call_action(action)
|
|
send("action_#{action}")
|
|
load_current_resource
|
|
end
|
|
|
|
def state
|
|
current_resource ? current_resource.state : {}
|
|
end
|
|
|
|
def ulimits_to_hash
|
|
return nil if new_resource.ulimits.nil?
|
|
new_resource.ulimits.map do |u|
|
|
name = u.split('=')[0]
|
|
soft = u.split('=')[1].split(':')[0]
|
|
hard = u.split('=')[1].split(':')[1]
|
|
{ 'Name' => name, 'Soft' => soft.to_i, 'Hard' => hard.to_i }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|