feat: initial commit

This commit is contained in:
amy 2025-04-01 17:40:03 +00:00
commit 38f495e3f4
457 changed files with 40577 additions and 0 deletions

View file

@ -0,0 +1,152 @@
# frozen_string_literal: true
require 'facter'
require 'json'
Facter.add(:docker_systemroot) do
confine osfamily: :windows
setcode do
Puppet::Util.get_env('SystemRoot')
end
end
Facter.add(:docker_program_files_path) do
confine osfamily: :windows
setcode do
Puppet::Util.get_env('ProgramFiles')
end
end
Facter.add(:docker_program_data_path) do
confine osfamily: :windows
setcode do
Puppet::Util.get_env('ProgramData')
end
end
Facter.add(:docker_user_temp_path) do
confine osfamily: :windows
setcode do
Puppet::Util.get_env('TEMP')
end
end
docker_command = if Facter.value(:kernel) == 'windows'
'powershell -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass -c docker'
else
'docker'
end
def interfaces
Facter.value(:interfaces).split(',')
end
Facter.add(:docker_version) do
confine { Facter::Core::Execution.which('docker') }
setcode do
value = Facter::Core::Execution.execute(
"#{docker_command} version --format '{{json .}}'", timeout: 90
)
JSON.parse(value)
end
end
Facter.add(:docker_client_version) do
setcode do
docker_version = Facter.value(:docker_version)
if docker_version
if docker_version['Client'].nil?
docker_version['Version']
else
docker_version['Client']['Version']
end
end
end
end
Facter.add(:docker_server_version) do
setcode do
docker_version = Facter.value(:docker_version)
if docker_version && !docker_version['Server'].nil? && docker_version['Server'].is_a?(Hash)
docker_version['Server']['Version']
else
nil
end
end
end
Facter.add(:docker_worker_join_token) do
confine { Facter::Core::Execution.which('docker') }
setcode do
# only run `docker swarm` commands if this node is in active in a cluster
docker_json_str = Facter::Core::Execution.execute(
"#{docker_command} info --format '{{json .}}'", timeout: 90
)
begin
docker = JSON.parse(docker_json_str)
if docker.fetch('Swarm', {})['LocalNodeState'] == 'active'
val = Facter::Core::Execution.execute(
"#{docker_command} swarm join-token worker -q", timeout: 90
)
end
rescue JSON::ParserError
nil
end
val
end
end
Facter.add(:docker_manager_join_token) do
confine { Facter::Core::Execution.which('docker') }
setcode do
# only run `docker swarm` commands if this node is in active in a cluster
docker_json_str = Facter::Core::Execution.execute(
"#{docker_command} info --format '{{json .}}'", timeout: 90
)
begin
docker = JSON.parse(docker_json_str)
if docker.fetch('Swarm', {})['LocalNodeState'] == 'active'
val = Facter::Core::Execution.execute(
"#{docker_command} swarm join-token manager -q", timeout: 90
)
end
rescue JSON::ParserError
nil
end
val
end
end
Facter.add(:docker) do
confine { Facter::Core::Execution.which('docker') }
setcode do
docker_version = Facter.value(:docker_client_version)
if docker_version&.match?(%r{\A(1\.1[3-9]|[2-9]|\d{2,})\.})
docker_json_str = Facter::Core::Execution.execute(
"#{docker_command} info --format '{{json .}}'", timeout: 90
)
begin
docker = JSON.parse(docker_json_str)
docker['network'] = {}
docker['network']['managed_interfaces'] = {}
network_list = Facter::Core::Execution.execute("#{docker_command} network ls | tail -n +2", timeout: 90)
docker_network_names = []
network_list.each_line { |line| docker_network_names.push line.split[1] }
docker_network_ids = []
network_list.each_line { |line| docker_network_ids.push line.split[0] }
docker_network_names.each do |network|
inspect = JSON.parse(Facter::Core::Execution.execute("#{docker_command} network inspect #{network}", timeout: 90))
docker['network'][network] = inspect[0]
network_id = docker['network'][network]['Id'][0..11]
interfaces.each do |iface|
docker['network']['managed_interfaces'][iface] = network if %r{#{network_id}}.match?(iface)
end
end
docker
rescue JSON::ParserError
nil
end
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
Puppet::Functions.create_function(:'docker::env') do
dispatch :env do
param 'Array', :args
return_type 'Array'
end
def env(args)
args
end
end

View file

@ -0,0 +1,155 @@
# frozen_string_literal: true
Puppet::Functions.create_function(:docker_params_changed) do
dispatch :detect_changes do
param 'Hash', :opts
return_type 'String'
end
def run_with_powershell(cmd)
"powershell.exe -Command \"& {#{cmd}}\" "
end
def remove_cidfile(cidfile, osfamily)
delete_command = if osfamily == 'windows'
run_with_powershell("del #{cidfile}")
else
"rm -f #{cidfile}"
end
_stdout, _stderr, _status = Open3.capture3(delete_command)
end
def start_container(name, osfamily)
start_command = if osfamily == 'windows'
run_with_powershell("docker start #{name}")
else
"docker start #{name}"
end
_stdout, _stderr, _status = Open3.capture3(start_command)
end
def stop_container(name, osfamily)
stop_command = if osfamily == 'windows'
run_with_powershell("docker stop #{name}")
else
"docker stop #{name}"
end
_stdout, _stderr, _status = Open3.capture3(stop_command)
end
def remove_container(name, osfamily, stop_wait_time, cidfile)
stop_command = if osfamily == 'windows'
run_with_powershell("docker stop --time=#{stop_wait_time} #{name}")
else
"docker stop --time=#{stop_wait_time} #{name}"
end
_stdout, _stderr, _status = Open3.capture3(stop_command)
remove_command = if osfamily == 'windows'
run_with_powershell("docker rm -v #{name}")
else
"docker rm -v #{name}"
end
_stdout, _stderr, _status = Open3.capture3(remove_command)
remove_cidfile(cidfile, osfamily)
end
def create_container(cmd, osfamily, image)
pull_command = if osfamily == 'windows'
run_with_powershell("docker pull #{image} -q")
else
"docker pull #{image} -q"
end
_stdout, _stderr, _status = Open3.capture3(pull_command)
create_command = if osfamily == 'windows'
run_with_powershell(cmd)
else
cmd
end
_stdout, _stderr, _status = Open3.capture3(create_command)
end
def detect_changes(opts)
require 'open3'
require 'json'
return_value = 'No changes detected'
if opts['sanitised_title'] && opts['osfamily']
stdout, _stderr, status = Open3.capture3("docker inspect #{opts['sanitised_title']}")
if status.to_s.include?('exit 0')
param_changed = false
inspect_hash = JSON.parse(stdout)[0]
# check if the image was changed
param_changed = true if opts['image'] && opts['image'] != inspect_hash['Config']['Image']
# check if something on volumes or mounts was changed(a new volume/mount was added or removed)
param_changed = true if opts['volumes'].is_a?(String) && opts['volumes'].include?(':') && opts['volumes'] != inspect_hash['Mounts'].to_a[0] && opts['osfamily'] != 'windows'
param_changed = true if opts['volumes'].is_a?(String) && !opts['volumes'].include?(':') && opts['volumes'] != inspect_hash['Config']['Volumes'].to_a[0] && opts['osfamily'] != 'windows'
param_changed = true if opts['volumes'].is_a?(String) && opts['volumes'].scan(%r{(?=:)}).count == 2 && opts['volumes'] != inspect_hash['Mounts'].to_a[0] && opts['osfamily'] == 'windows'
if opts['volumes'].is_a?(String) && opts['volumes'].scan(%r{(?=:)}).count == 1 && opts['volumes'] != inspect_hash['Config']['Volumes'].to_a[0] && opts['osfamily'] == 'windows'
param_changed = true
end
pp_paths = opts['volumes'].reject { |item| item.include?(':') } if opts['volumes'].is_a?(Array) && opts['osfamily'] != 'windows'
pp_mounts = opts['volumes'].select { |item| item.include?(':') } if opts['volumes'].is_a?(Array) && opts['osfamily'] != 'windows'
pp_paths = opts['volumes'].select { |item| item.scan(%r{(?=:)}).count == 1 } if opts['volumes'].is_a?(Array) && opts['osfamily'] == 'windows'
pp_mounts = opts['volumes'].select { |item| item.scan(%r{(?=:)}).count == 2 } if opts['volumes'].is_a?(Array) && opts['osfamily'] == 'windows'
inspect_paths = if inspect_hash['Config']['Volumes']
inspect_hash['Config']['Volumes'].keys
else
[]
end
param_changed = true if pp_paths != inspect_paths
names = inspect_hash['Mounts'].map { |item| item.values[1] } if inspect_hash['Mounts']
pp_names = pp_mounts.map { |item| item.split(':')[0] } if pp_mounts
names = names.select { |item| pp_names.include?(item) } if names && pp_names
destinations = inspect_hash['Mounts'].map { |item| item.values[3] } if inspect_hash['Mounts']
pp_destinations = pp_mounts.map { |item| item.split(':')[1] } if pp_mounts && opts['osfamily'] != 'windows'
pp_destinations = pp_mounts.map { |item| "#{item.split(':')[1].downcase}:#{item.split(':')[2]}" } if pp_mounts && opts['osfamily'] == 'windows'
destinations = destinations.select { |item| pp_destinations.include?(item) } if destinations && pp_destinations
param_changed = true if pp_names != names
param_changed = true if pp_destinations != destinations
param_changed = true if pp_mounts != [] && inspect_hash['Mounts'].nil?
# check if something on ports was changed(some ports were added or removed)
ports = inspect_hash['HostConfig']['PortBindings'].keys
ports = ports.map { |item| item.split('/')[0] }
pp_ports = opts['ports'].sort if opts['ports'].is_a?(Array)
pp_ports = [opts['ports']] if opts['ports'].is_a?(String)
param_changed = true if pp_ports && pp_ports != ports
if param_changed
remove_container(opts['sanitised_title'], opts['osfamily'], opts['stop_wait_time'], opts['cidfile'])
create_container(opts['command'], opts['osfamily'], opts['image'])
return_value = 'Param changed'
end
else
create_container(opts['command'], opts['osfamily'], opts['image']) unless File.exist?(opts['cidfile'])
_stdout, _stderr, status = Open3.capture3("docker inspect #{opts['sanitised_title']}")
unless status.to_s.include?('exit 0')
remove_cidfile(opts['cidfile'], opts['osfamily'])
create_container(opts['command'], opts['osfamily'], opts['image'])
end
return_value = 'No changes detected'
end
else
return_value = 'Arg required missing'
end
if opts['container_running']
start_container(opts['sanitised_title'], opts['osfamily'])
else
stop_container(opts['sanitised_title'], opts['osfamily'])
end
return_value
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'shellwords'
#
# docker_exec_flags.rb
#
module Puppet::Parser::Functions
# Transforms a hash into a string of docker exec flags
newfunction(:docker_exec_flags, type: :rvalue) do |args|
opts = args[0] || {}
flags = []
flags << '--detach=true' if opts['detach']
flags << '--interactive=true' if opts['interactive']
flags << '--tty=true' if opts['tty']
opts['env']&.each do |namevaluepair|
flags << "--env #{namevaluepair}"
end
flags.flatten.join(' ')
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'shellwords'
#
# docker_plugin_remove_flags.rb
#
module Puppet::Parser::Functions
# Transforms a hash into a string of docker plugin remove flags
newfunction(:docker_plugin_enable_flags, type: :rvalue) do |args|
opts = args[0] || {}
flags = []
flags << '--force' if opts['force_remove'] == true
if opts['plugin_alias'] && opts['plugin_alias'].to_s != 'undef'
flags << "'#{opts['plugin_alias']}'"
elsif opts['plugin_name'] && opts['plugin_name'].to_s != 'undef'
flags << "'#{opts['plugin_name']}'"
end
flags.flatten.join(' ')
end
end

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'shellwords'
#
# docker_plugin_install_flags.rb
#
module Puppet::Parser::Functions
# Transforms a hash into a string of docker plugin install flags
newfunction(:docker_plugin_install_flags, type: :rvalue) do |args|
opts = args[0] || {}
flags = []
flags << "--alias #{opts['plugin_alias']}" if opts['plugin_alias'] && opts['plugin_alias'].to_s != 'undef'
flags << '--disable' if opts['disable_on_install'] == true
flags << '--disable-content-trust' if opts['disable_content_trust'] == true
flags << '--grant-all-permissions' if opts['grant_all_permissions'] == true
flags << "'#{opts['plugin_name']}'" if opts['plugin_name'] && opts['plugin_name'].to_s != 'undef'
if opts['settings'].is_a? Array
opts['settings'].each do |setting|
flags << setting.to_s
end
end
flags.flatten.join(' ')
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'shellwords'
#
# docker_plugin_remove_flags.rb
#
module Puppet::Parser::Functions
# Transforms a hash into a string of docker plugin remove flags
newfunction(:docker_plugin_remove_flags, type: :rvalue) do |args|
opts = args[0] || {}
flags = []
flags << '--force' if opts['force_remove'] == true
flags << "'#{opts['plugin_name']}'" if opts['plugin_name'] && opts['plugin_name'].to_s != 'undef'
flags.flatten.join(' ')
end
end

View file

@ -0,0 +1,96 @@
# frozen_string_literal: true
#
# docker_run_flags.rb
#
module Puppet::Parser::Functions
newfunction(:'docker::escape', type: :rvalue) do |args|
subject = args[0]
escape_function = if self['facts'] && self['facts']['os']['family'] == 'windows'
'stdlib::powershell_escape'
else
'stdlib::shell_escape'
end
call_function(escape_function, subject)
end
# Transforms a hash into a string of docker flags
newfunction(:docker_run_flags, type: :rvalue) do |args|
opts = args[0] || {}
flags = []
flags << "-u #{call_function('docker::escape', [opts['username']])}" if opts['username']
flags << "-h #{call_function('docker::escape', [opts['hostname']])}" if opts['hostname']
flags << "--restart '#{opts['restart']}'" if opts['restart']
if opts['net']
if opts['net'].is_a? String
flags << "--net #{call_function('docker::escape', [opts['net']])}"
elsif opts['net'].is_a? Array
flags += opts['net'].map { |item| ["--net #{call_function('docker::escape', [item])}"] }
end
end
flags << "-m #{opts['memory_limit']}" if opts['memory_limit']
cpusets = [opts['cpuset']].flatten.compact
unless cpusets.empty?
value = cpusets.join(',')
flags << "--cpuset-cpus=#{value}"
end
flags << '-n false' if opts['disable_network']
flags << '--privileged' if opts['privileged']
flags << "--health-cmd='#{opts['health_check_cmd']}'" if opts['health_check_cmd'] && opts['health_check_cmd'].to_s != 'undef'
flags << "--health-interval=#{opts['health_check_interval']}s" if opts['health_check_interval'] && opts['health_check_interval'].to_s != 'undef'
flags << '-t' if opts['tty']
flags << '--read-only=true' if opts['read_only']
params_join_char = if opts['osfamily'] && opts['osfamily'].to_s != 'undef'
opts['osfamily'].casecmp('windows').zero? ? " `\n" : " \\\n"
else
" \\\n"
end
multi_flags = ->(values, fmt) {
filtered = [values].flatten.compact
filtered.map { |val| (fmt + params_join_char) % call_function('docker::escape', [val]) }
}
[
['--dns %s', 'dns'],
['--dns-search %s', 'dns_search'],
['--expose=%s', 'expose'],
['--link %s', 'links'],
['--lxc-conf=%s', 'lxc_conf'],
['--volumes-from %s', 'volumes_from'],
['-e %s', 'env'],
['--env-file %s', 'env_file'],
['-p %s', 'ports'],
['-l %s', 'labels'],
['--add-host %s', 'hostentries'],
['-v %s', 'volumes'],
].each do |(format, key)|
values = opts[key]
new_flags = multi_flags.call(values, format)
flags.concat(new_flags)
end
opts['extra_params'].each do |param|
flags << param
end
# Some software (inc systemd) will truncate very long lines using glibc's
# max line length. Wrap options across multiple lines with '\' to avoid
flags.flatten.join(params_join_char)
end
end

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'shellwords'
#
# docker_secrets_flags.rb
#
module Puppet::Parser::Functions
# Transforms a hash into a string of docker swarm init flags
newfunction(:docker_secrets_flags, type: :rvalue) do |args|
opts = args[0] || {}
flags = []
flags << 'create' if opts['ensure'].to_s == 'present'
flags << "'#{opts['secret_name']}'" if opts['secret_name'] && opts['secret_name'].to_s != 'undef'
flags << "'#{opts['secret_path']}'" if opts['secret_path'] && opts['secret_path'].to_s != 'undef'
multi_flags = ->(values, format) {
filtered = [values].flatten.compact
filtered.map { |val| format + (" \\\n" % val) }
}
[
['-l %s', 'label'],
].each do |(format, key)|
values = opts[key]
new_flags = multi_flags.call(values, format)
flags.concat(new_flags)
end
flags.flatten.join(' ')
end
end

View file

@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'shellwords'
#
# docker_service_flags.rb
#
module Puppet::Parser::Functions
# Transforms a hash into a string of docker swarm init flags
newfunction(:docker_service_flags, type: :rvalue) do |args|
opts = args[0] || {}
flags = []
flags << "'#{opts['service_name']}'" if opts['service_name'] && opts['service_name'].to_s != 'undef'
flags << '--detach' if opts['detach'].to_s != 'false'
if opts['env'].is_a? Array
opts['env'].each do |env|
flags << "--env '#{env}'"
end
end
if opts['label'].is_a? Array
opts['label'].each do |label|
flags << "--label #{label}"
end
end
if opts['mounts'].is_a? Array
opts['mounts'].each do |mount|
flags << "--mount #{mount}"
end
end
if opts['networks'].is_a? Array
opts['networks'].each do |network|
flags << "--network #{network}"
end
end
if opts['publish'].is_a? Array
opts['publish'].each do |port|
flags << "--publish #{port}"
end
elsif opts['publish'] && opts['publish'].to_s != 'undef'
flags << "--publish '#{opts['publish']}'"
end
flags << "--replicas '#{opts['replicas']}'" if opts['replicas'] && opts['replicas'].to_s != 'undef'
flags << '--tty' if opts['tty'].to_s != 'false'
flags << "--user '#{opts['user']}'" if opts['user'] && opts['user'].to_s != 'undef'
flags << "--workdir '#{opts['workdir']}'" if opts['workdir'] && opts['workdir'].to_s != 'undef'
if opts['extra_params'].is_a? Array
opts['extra_params'].each do |param|
flags << param
end
end
flags << "-H '#{opts['host_socket']}'" if opts['host_socket'] && opts['host_socket'].to_s != 'undef'
if opts['registry_mirror'].is_a? Array
opts['registry_mirror'].each do |param|
flags << "--registry-mirror='#{param}'"
end
elsif opts['registry_mirror'] && opts['registry_mirror'].to_s != 'undef'
flags << "--registry-mirror='#{opts['registry_mirror']}'"
end
flags << "'#{opts['image']}'" if opts['image'] && opts['image'].to_s != 'undef'
if opts['command'].is_a? Array
flags << opts['command'].join(' ')
elsif opts['command'] && opts['command'].to_s != 'undef'
flags << opts['command'].to_s
end
flags.flatten.join(' ')
end
end

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'shellwords'
#
# docker_stack_flags.rb
#
module Puppet::Parser::Functions
# Transforms a hash into a string of docker stack flags
newfunction(:docker_stack_flags, type: :rvalue) do |args|
opts = args[0] || {}
flags = []
flags << "--bundle-file '#{opts['bundle_file']}'" if opts['bundle_file'] && opts['bundle_file'].to_s != 'undef'
if opts['compose_files'] && opts['compose_files'].to_s != 'undef'
opts['compose_files'].each do |file|
flags << "--compose-file '#{file}'"
end
end
flags << "--resolve-image '#{opts['resolve_image']}'" if opts['resolve_image'] && opts['resolve_image'].to_s != 'undef'
flags << '--prune' if opts['prune'] && opts['prune'].to_s != 'undef'
flags << '--with-registry-auth' if opts['with_registry_auth'] && opts['with_registry_auth'].to_s != 'undef'
flags.flatten.join(' ')
end
end

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'shellwords'
#
# docker_swarm_init_flags.rb
#
module Puppet::Parser::Functions
# Transforms a hash into a string of docker swarm init flags
newfunction(:docker_swarm_init_flags, type: :rvalue) do |args|
opts = args[0] || {}
flags = []
flags << 'init' if opts['init'].to_s != 'false'
flags << "--advertise-addr '#{opts['advertise_addr']}'" if opts['advertise_addr'] && opts['advertise_addr'].to_s != 'undef'
flags << '--autolock' if opts['autolock'].to_s != 'false'
flags << "--cert-expiry '#{opts['cert_expiry']}'" if opts['cert_expiry'] && opts['cert_expiry'].to_s != 'undef'
if opts['default_addr_pool'].is_a? Array
opts['default_addr_pool'].each do |default_addr_pool|
flags << "--default-addr-pool #{default_addr_pool}"
end
end
flags << "--default-addr-pool-mask-length '#{opts['default_addr_pool_mask_length']}'" if opts['default_addr_pool_mask_length'] && opts['default_addr_pool_mask_length'].to_s != 'undef'
flags << "--dispatcher-heartbeat '#{opts['dispatcher_heartbeat']}'" if opts['dispatcher_heartbeat'] && opts['dispatcher_heartbeat'].to_s != 'undef'
flags << "--external-ca '#{opts['external_ca']}'" if opts['external_ca'] && opts['external_ca'].to_s != 'undef'
flags << '--force-new-cluster' if opts['force_new_cluster'].to_s != 'false'
flags << "--listen-addr '#{opts['listen_addr']}'" if opts['listen_addr'] && opts['listen_addr'].to_s != 'undef'
flags << "--max-snapshots '#{opts['max_snapshots']}'" if opts['max_snapshots'] && opts['max_snapshots'].to_s != 'undef'
flags << "--snapshot-interval '#{opts['snapshot_interval']}'" if opts['snapshot_interval'] && opts['snapshot_interval'].to_s != 'undef'
flags.flatten.join(' ')
end
end

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'shellwords'
#
# docker_swarm_join_flags.rb
#
module Puppet::Parser::Functions
# Transforms a hash into a string of docker swarm init flags
newfunction(:docker_swarm_join_flags, type: :rvalue) do |args|
opts = args[0] || {}
flags = []
flags << 'join' if opts['join'].to_s != 'false'
flags << "--advertise-addr '#{opts['advertise_addr']}'" if opts['advertise_addr'] && opts['advertise_addr'].to_s != 'undef'
flags << "--listen-addr \"#{opts['listen_addr']}\"" if opts['listen_addr'] && opts['listen_addr'].to_s != 'undef'
flags << "--token '#{opts['token']}'" if opts['token'] && opts['token'].to_s != 'undef'
flags.flatten.join(' ')
end
end

View file

@ -0,0 +1,109 @@
# frozen_string_literal: true
require 'deep_merge'
Puppet::Type.type(:docker_compose).provide(:ruby) do
desc 'Support for Puppet running Docker Compose'
mk_resource_methods
has_command(:docker, 'docker')
def set_tmpdir
return unless resource[:tmpdir]
# Check if the the tmpdir target exists
Puppet.warning("#{resource[:tmpdir]} (defined as docker_compose tmpdir) does not exist") unless Dir.exist?(resource[:tmpdir])
# Set TMPDIR environment variable only if defined among resources and exists
ENV['TMPDIR'] = resource[:tmpdir] if Dir.exist?(resource[:tmpdir])
end
def exists?
Puppet.info("Checking for compose project #{name}")
compose_services = {}
compose_containers = []
set_tmpdir
# get merged config using docker-compose config
args = ['compose', compose_files, '-p', name, 'config'].insert(3, resource[:options]).compact
compose_output = Puppet::Util::Yaml.safe_load(execute([command(:docker)] + args, combine: false), [Symbol])
containers = docker([
'ps',
'--format',
"'{{.Label \"com.docker.compose.service\"}}-{{.Image}}'",
'--filter',
"label=com.docker.compose.project=#{name}",
]).split("\n")
compose_containers.push(*containers)
compose_services = compose_output['services']
return false if compose_services.count != compose_containers.uniq.count
counts = Hash[*compose_services.each.map { |key, array|
image = array['image'] || get_image(key, compose_services)
Puppet.info("Checking for compose service #{key} #{image}")
[key, compose_containers.count("'#{key}-#{image}'")]
}.flatten]
# No containers found for the project
if counts.empty? ||
# Containers described in the compose file are not running
counts.any? { |_k, v| v.zero? } ||
# The scaling factors in the resource do not match the number of running containers
(resource[:scale] && counts.merge(resource[:scale]) != counts)
false
else
true
end
end
def get_image(service_name, compose_services)
image = compose_services[service_name]['image']
unless image
if compose_services[service_name]['extends']
image = get_image(compose_services[service_name]['extends'], compose_services)
elsif compose_services[service_name]['build']
image = "#{name}_#{service_name}"
end
end
image
end
def create
Puppet.info("Running compose project #{name}")
args = ['compose', compose_files, '-p', name, 'up', '-d', '--remove-orphans'].insert(3, resource[:options]).insert(5, resource[:up_args]).compact
docker(args)
return unless resource[:scale]
instructions = resource[:scale].map { |k, v| "#{k}=#{v}" }
Puppet.info("Scaling compose project #{name}: #{instructions.join(' ')}")
args = ['compose', compose_files, '-p', name, 'scale'].insert(3, resource[:options]).compact + instructions
docker(args)
end
def destroy
Puppet.info("Removing all containers for compose project #{name}")
kill_args = ['compose', compose_files, '-p', name, 'kill'].insert(3, resource[:options]).compact
docker(kill_args)
rm_args = ['compose', compose_files, '-p', name, 'rm', '--force', '-v'].insert(3, resource[:options]).compact
docker(rm_args)
end
def restart
return unless exists?
Puppet.info("Rebuilding and Restarting all containers for compose project #{name}")
kill_args = ['compose', compose_files, '-p', name, 'kill'].insert(3, resource[:options]).compact
docker(kill_args)
build_args = ['compose', compose_files, '-p', name, 'build'].insert(3, resource[:options]).compact
docker(build_args)
create
end
def compose_files
resource[:compose_files].map { |x| ['-f', x] }.flatten
end
end

View file

@ -0,0 +1,95 @@
# frozen_string_literal: true
require 'json'
Puppet::Type.type(:docker_network).provide(:ruby) do
desc 'Support for Docker Networking'
mk_resource_methods
has_command(:docker, 'docker')
def network_conf
flags = ['network', 'create']
multi_flags = ->(values, format) {
filtered = [values].flatten.compact
filtered.map { |val| format % val }
}
[
['--driver=%s', :driver],
['--subnet=%s', :subnet],
['--gateway=%s', :gateway],
['--ip-range=%s', :ip_range],
['--ipam-driver=%s', :ipam_driver],
['--aux-address=%s', :aux_address],
['--opt=%s', :options],
].each do |(format, key)|
values = resource[key]
new_flags = multi_flags.call(values, format)
flags.concat(new_flags)
end
if defined?(resource[:additional_flags])
additional_flags = []
if resource[:additional_flags].is_a?(String)
additional_flags = resource[:additional_flags].split
elsif resource[:additional_flags].is_a?(Array)
additional_flags = resource[:additional_flags]
end
additional_flags.each do |additional_flag|
flags << additional_flag
end
end
flags << resource[:name]
end
def self.instances
output = docker(['network', 'ls'])
lines = output.split("\n")
lines.shift # remove header row
lines.map do |line|
_, name, driver = line.split
inspect = docker(['network', 'inspect', name])
obj = JSON.parse(inspect).first
ipam_driver = (obj['IPAM']['Driver'] unless obj['IPAM']['Driver'].nil?)
subnet = (obj['IPAM']['Config'].first['Subnet'] if !(obj['IPAM']['Config'].nil? || obj['IPAM']['Config'].empty?) && (obj['IPAM']['Config'].first.key? 'Subnet'))
new(
name: name,
id: obj['Id'],
ipam_driver: ipam_driver,
subnet: subnet,
ensure: :present,
driver: driver,
)
end
end
def self.prefetch(resources)
instances.each do |prov|
if resource = resources[prov.name] # rubocop:disable Lint/AssignmentInCondition
resource.provider = prov
end
end
end
def flush
raise Puppet::Error, _('Docker network does not support mutating existing networks') if !@property_hash.empty? && @property_hash[:ensure] != :absent
end
def exists?
Puppet.info("Checking if docker network #{name} exists")
@property_hash[:ensure] == :present
end
def create
Puppet.info("Creating docker network #{name}")
docker(network_conf)
end
def destroy
Puppet.info("Removing docker network #{name}")
docker(['network', 'rm', name])
end
end

View file

@ -0,0 +1,88 @@
# frozen_string_literal: true
require 'deep_merge'
Puppet::Type.type(:docker_stack).provide(:ruby) do
desc 'Support for Puppet running Docker Stacks'
mk_resource_methods
has_command(:docker, 'docker')
def exists?
Puppet.info("Checking for stack #{name}")
stack_services = {}
stack_containers = []
resource[:compose_files].each do |file|
compose_file = YAML.safe_load(File.read(file), [], [], true)
# rubocop:disable Style/StringLiterals
containers = docker([
'ps',
'--format',
"{{.Label \"com.docker.swarm.service.name\"}}-{{.Image}}",
'--filter',
"label=com.docker.stack.namespace=#{name}",
]).split("\n").each do |c|
c.slice!("#{name}_")
end
stack_containers.push(*containers)
stack_containers.uniq!
# rubocop:enable Style/StringLiterals
case compose_file['version']
when %r{^3(\.[0-7])?$}
stack_services.merge!(compose_file['services'])
else
raise(Puppet::Error, "Unsupported docker compose file syntax version \"#{compose_file['version']}\"!")
end
end
return false if stack_services.count != stack_containers.count
counts = Hash[*stack_services.each.map { |key, array|
image = array['image'] || get_image(key, stack_services)
image = "#{image}:latest" unless image.include?(':')
Puppet.info("Checking for compose service #{key} #{image}")
["#{key}-#{image}", stack_containers.count("#{key}-#{image}")]
}.flatten]
# No containers found for the project
if counts.empty? ||
# Containers described in the compose file are not running
counts.any? { |_k, v| v.zero? }
false
else
true
end
end
def get_image(service_name, stack_services)
image = stack_services[service_name]['image']
unless image
if stack_services[service_name]['extends']
image = get_image(stack_services[service_name]['extends'], stack_services)
elsif stack_services[service_name]['build']
image = "#{name}_#{service_name}"
end
end
image
end
def create
Puppet.info("Running stack #{name}")
args = ['stack', 'deploy', compose_files, name].insert(1, bundle_file).insert(4, resource[:up_args]).compact
docker(args)
end
def destroy
Puppet.info("Removing docker stack #{name}")
rm_args = ['stack', 'rm', name]
docker(rm_args)
end
def bundle_file
return resource[:bundle_file].map { |x| ['-c', x] }.flatten unless resource[:bundle_file].nil?
end
def compose_files
resource[:compose_files].map { |x| ['-c', x] }.flatten
end
end

View file

@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'json'
Puppet::Type.type(:docker_volume).provide(:ruby) do
desc 'Support for Docker Volumes'
mk_resource_methods
has_command(:docker, 'docker')
def volume_conf
flags = ['volume', 'create']
multi_flags = ->(values, format) {
filtered = [values].flatten.compact
filtered.map { |val| format % val }
}
[
['--driver=%s', :driver],
['--opt=%s', :options],
].each do |(format, key)|
values = resource[key]
new_flags = multi_flags.call(values, format)
flags.concat(new_flags)
end
flags << resource[:name]
end
def self.instances
output = docker(['volume', 'ls'])
lines = output.split("\n")
lines.shift # remove header row
lines.map do |line|
driver, name = line.split
inspect = docker(['volume', 'inspect', name])
obj = JSON.parse(inspect).first
new(
name: name,
mountpoint: obj['Mountpoint'],
options: obj['Options'],
ensure: :present,
driver: driver,
)
end
end
def self.prefetch(resources)
instances.each do |prov|
if (resource = resources[prov.name])
resource.provider = prov
end
end
end
def exists?
Puppet.info("Checking if docker volume #{name} exists")
@property_hash[:ensure] == :present
end
def create
Puppet.info("Creating docker volume #{name}")
docker(volume_conf)
end
def destroy
Puppet.info("Removing docker volume #{name}")
docker(['volume', 'rm', name])
end
end

View file

@ -0,0 +1,61 @@
# frozen_string_literal: true
Puppet::Type.newtype(:docker_compose) do
@doc = 'A type representing a Docker Compose file'
ensurable
def refresh
provider.restart
end
newparam(:scale) do
desc 'A hash of compose services and number of containers.'
validate do |value|
raise _('scale should be a Hash') unless value.is_a? Hash
raise _('The name of the compose service in scale should be a String') unless value.all? { |k, _v| k.is_a? String }
raise _('The number of containers in scale should be an Integer') unless value.all? { |_k, v| v.is_a? Integer }
end
end
newparam(:options) do
desc 'Additional options to be passed directly to docker-compose.'
validate do |value|
raise _('options should be an Array') unless value.is_a? Array
end
end
newparam(:up_args) do
desc 'Arguments to be passed directly to docker-compose up.'
validate do |value|
raise _('up_args should be a String') unless value.is_a? String
end
end
newparam(:compose_files, array_matching: :all) do
desc 'An array of Docker Compose Files paths.'
validate do |value|
raise _('compose files should be an array') unless value.is_a? Array
end
end
newparam(:name) do
isnamevar
desc 'The name of the project'
end
newparam(:tmpdir) do
desc "Override the temporary directory used by docker-compose.
This property is useful when the /tmp directory has been mounted
with the noexec option. Or is otherwise being prevented It allows the module consumer to redirect
docker-composes temporary files to a known directory.
The directory passed to this property must exist and be accessible
by the user that is executing the puppet agent.
"
validate do |value|
raise _('tmpdir should be a String') unless value.is_a? String
end
end
end

View file

@ -0,0 +1,50 @@
# frozen_string_literal: true
Puppet::Type.newtype(:docker_network) do
@doc = 'Type representing a Docker network'
ensurable
newparam(:name) do
isnamevar
desc 'The name of the network'
end
newproperty(:driver) do
desc 'The network driver used by the network'
end
newparam(:subnet, array_matching: :all) do
desc 'The subnet in CIDR format that represents a network segment'
end
newparam(:gateway) do
desc 'An ipv4 or ipv6 gateway for the server subnet'
end
newparam(:ip_range) do
desc 'The range of IP addresses used by the network'
end
newproperty(:ipam_driver) do
desc 'The IPAM (IP Address Management) driver'
end
newparam(:aux_address) do
desc 'Auxiliary ipv4 or ipv6 addresses used by the Network driver'
end
newparam(:options) do
desc 'Additional options for the network driver'
end
newparam(:additional_flags) do
desc "Additional flags for the 'docker network create'"
end
newproperty(:id) do
desc 'The ID of the network provided by Docker'
validate do |value|
raise(Puppet::ParseError, "#{value} is read-only and is only available via puppet resource.")
end
end
end

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
Puppet::Type.newtype(:docker_stack) do
@doc = 'A type representing a Docker Stack'
ensurable
newparam(:bundle_file) do
desc 'Path to a Distributed Application Bundle file.'
validate do |value|
raise _('bundle files should be a string') unless value.is_a? String
end
end
newparam(:compose_files, array_matching: :all) do
desc 'An array of Docker Compose Files paths.'
validate do |value|
raise _('compose files should be an array') unless value.is_a? Array
end
end
newparam(:up_args) do
desc 'Arguments to be passed directly to docker stack deploy.'
validate do |value|
raise _('up_args should be a String') unless value.is_a? String
end
end
newparam(:name) do
isnamevar
desc 'The name of the stack'
end
end

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
Puppet::Type.newtype(:docker_volume) do
@doc = 'A type representing a Docker volume'
ensurable
newparam(:name) do
isnamevar
desc 'The name of the volume'
end
newproperty(:driver) do
desc 'The volume driver used by the volume'
end
newparam(:options) do
desc 'Additional options for the volume driver'
end
newproperty(:mountpoint) do
desc 'The location that the volume is mounted to'
validate do |value|
raise(Puppet::ParseError, "#{value} is read-only and is only available via puppet resource.")
end
end
end