Skip to content

Commit

Permalink
Add Docker Swarm support
Browse files Browse the repository at this point in the history
- Adds swarm init, join, service & token resources

Signed-off-by: Dan Webb <dan.webb@damacus.io>
  • Loading branch information
damacus committed Dec 14, 2024
1 parent ca21f85 commit b7d2cd2
Show file tree
Hide file tree
Showing 16 changed files with 725 additions and 0 deletions.
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruby system
49 changes: 49 additions & 0 deletions kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,55 @@ suites:
- recipe[docker_test::default]
- recipe[docker_test::registry]

####################
# swarm testing
####################

- name: swarm
driver:
network:
- ["private_network", {ip: "192.168.56.10"}]
provisioner:
enforce_idempotency: false
multiple_converge: 1
attributes:
docker:
version: '20.10.11'
swarm:
init:
advertise_addr: '192.168.56.10'
listen_addr: '0.0.0.0:2377'
rotate_token: true
service:
name: 'web'
image: 'nginx:latest'
publish: ['80:80']
replicas: 2
run_list:
- recipe[docker_test::swarm_default]
- recipe[docker_test::swarm_init]
- recipe[docker_test::swarm_service]

- name: swarm_worker
driver:
network:
- ["private_network", {ip: "192.168.56.11"}]
provisioner:
enforce_idempotency: false
multiple_converge: 1
attributes:
docker:
version: '20.10.11'
swarm:
join:
manager_ip: '192.168.56.10:2377'
advertise_addr: '192.168.56.11'
listen_addr: '0.0.0.0:2377'
# Token will be obtained from the manager node
run_list:
- recipe[docker_test::swarm_default]
- recipe[docker_test::swarm_join]

#############################
# quick service smoke testing
#############################
Expand Down
58 changes: 58 additions & 0 deletions libraries/helpers_swarm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module DockerCookbook
module DockerHelpers
module Swarm
def swarm_init_cmd(resource = nil)
cmd = %w(docker swarm init)
cmd << "--advertise-addr #{resource.advertise_addr}" if resource && resource.advertise_addr
cmd << "--listen-addr #{resource.listen_addr}" if resource && resource.listen_addr
cmd << '--force-new-cluster' if resource && resource.force_new_cluster
cmd
end

def swarm_join_cmd(resource = nil)
cmd = %w(docker swarm join)
cmd << "--token #{resource.token}" if resource
cmd << "--advertise-addr #{resource.advertise_addr}" if resource && resource.advertise_addr
cmd << "--listen-addr #{resource.listen_addr}" if resource && resource.listen_addr
cmd << resource.manager_ip if resource
cmd
end

def swarm_leave_cmd(resource = nil)
cmd = %w(docker swarm leave)
cmd << '--force' if resource && resource.force
cmd
end

def swarm_token_cmd(token_type)
raise 'Token type must be worker or manager' unless %w(worker manager).include?(token_type)
%w(docker swarm join-token -q) << token_type
end

def swarm_member?(resource = nil)
cmd = Mixlib::ShellOut.new('docker info --format "{{ .Swarm.LocalNodeState }}"')
cmd.run_command
return false if cmd.error?
cmd.stdout.strip == 'active'
end

def swarm_manager?(resource = nil)
return false unless swarm_member?
cmd = Mixlib::ShellOut.new('docker info --format "{{ .Swarm.ControlAvailable }}"')
cmd.run_command
return false if cmd.error?
cmd.stdout.strip == 'true'
end

def swarm_worker?(resource = nil)
swarm_member? && !swarm_manager?
end

def service_exists?(name)
cmd = Mixlib::ShellOut.new("docker service inspect #{name}")
cmd.run_command
!cmd.error?
end
end
end
end
35 changes: 35 additions & 0 deletions resources/swarm_init.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
unified_mode true

include DockerCookbook::DockerHelpers::Swarm

resource_name :docker_swarm_init
provides :docker_swarm_init

property :advertise_addr, String
property :listen_addr, String
property :force_new_cluster, [true, false], default: false
property :autolock, [true, false], default: false

action :init do
return if swarm_member?(new_resource)

converge_by 'initializing docker swarm' do
cmd = Mixlib::ShellOut.new(swarm_init_cmd(new_resource).join(' '))
cmd.run_command
if cmd.error?
raise "Failed to initialize swarm: #{cmd.stderr}"
end
end
end

action :leave do
return unless swarm_member?(new_resource)

converge_by 'leaving docker swarm' do
cmd = Mixlib::ShellOut.new('docker swarm leave --force')
cmd.run_command
if cmd.error?
raise "Failed to leave swarm: #{cmd.stderr}"
end
end
end
36 changes: 36 additions & 0 deletions resources/swarm_join.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
unified_mode true

include DockerCookbook::DockerHelpers::Swarm

resource_name :docker_swarm_join
provides :docker_swarm_join

property :token, String, required: true
property :manager_ip, String, required: true
property :advertise_addr, String
property :listen_addr, String
property :data_path_addr, String

action :join do
return if swarm_member?(new_resource)

converge_by 'joining docker swarm' do
cmd = Mixlib::ShellOut.new(swarm_join_cmd(new_resource).join(' '))
cmd.run_command
if cmd.error?
raise "Failed to join swarm: #{cmd.stderr}"
end
end
end

action :leave do
return unless swarm_member?(new_resource)

converge_by 'leaving docker swarm' do
cmd = Mixlib::ShellOut.new('docker swarm leave --force')
cmd.run_command
if cmd.error?
raise "Failed to leave swarm: #{cmd.stderr}"
end
end
end
121 changes: 121 additions & 0 deletions resources/swarm_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
unified_mode true

include DockerCookbook::DockerHelpers::Swarm

resource_name :docker_swarm_service
provides :docker_swarm_service

property :service_name, String, name_property: true
property :image, String, required: true
property :command, [String, Array]
property :replicas, Integer, default: 1
property :env, [Array], default: []
property :labels, [Hash], default: {}
property :mounts, [Array], default: []
property :networks, [Array], default: []
property :ports, [Array], default: []
property :constraints, [Array], default: []
property :secrets, [Array], default: []
property :configs, [Array], default: []
property :restart_policy, Hash, default: { condition: 'any' }

# Health check
property :healthcheck_cmd, String
property :healthcheck_interval, String
property :healthcheck_timeout, String
property :healthcheck_retries, Integer

load_current_value do |new_resource|
cmd = Mixlib::ShellOut.new("docker service inspect #{new_resource.service_name}")
cmd.run_command
if cmd.error?
current_value_does_not_exist!
else
service_info = JSON.parse(cmd.stdout).first
image service_info['Spec']['TaskTemplate']['ContainerSpec']['Image']
command service_info['Spec']['TaskTemplate']['ContainerSpec']['Command']
env service_info['Spec']['TaskTemplate']['ContainerSpec']['Env']
replicas service_info['Spec']['Mode']['Replicated']['Replicas']
end
end

action :create do
return unless swarm_manager?(new_resource)

converge_if_changed do
cmd = create_service_cmd(new_resource)

converge_by "creating service #{new_resource.service_name}" do
shell_out!(cmd.join(' '))
end
end
end

action :update do
return unless swarm_manager?(new_resource)
return unless service_exists?(new_resource)

converge_if_changed do
cmd = update_service_cmd(new_resource)

converge_by "updating service #{new_resource.service_name}" do
shell_out!(cmd.join(' '))
end
end
end

action :delete do
return unless swarm_manager?(new_resource)
return unless service_exists?(new_resource)

converge_by "deleting service #{new_resource.service_name}" do
shell_out!("docker service rm #{new_resource.service_name}")
end
end

action_class do
def service_exists?(new_resource)
cmd = Mixlib::ShellOut.new("docker service inspect #{new_resource.service_name}")
cmd.run_command
!cmd.error?
end

def create_service_cmd(new_resource)
cmd = %w(docker service create)
cmd << "--name #{new_resource.service_name}"
cmd << "--replicas #{new_resource.replicas}"

new_resource.env.each { |e| cmd << "--env #{e}" }
new_resource.labels.each { |k, v| cmd << "--label #{k}=#{v}" }
new_resource.mounts.each { |m| cmd << "--mount #{m}" }
new_resource.networks.each { |n| cmd << "--network #{n}" }
new_resource.ports.each { |p| cmd << "--publish #{p}" }
new_resource.constraints.each { |c| cmd << "--constraint #{c}" }

if new_resource.restart_policy
cmd << "--restart-condition #{new_resource.restart_policy[:condition]}"
cmd << "--restart-delay #{new_resource.restart_policy[:delay]}" if new_resource.restart_policy[:delay]
cmd << "--restart-max-attempts #{new_resource.restart_policy[:max_attempts]}" if new_resource.restart_policy[:max_attempts]
cmd << "--restart-window #{new_resource.restart_policy[:window]}" if new_resource.restart_policy[:window]
end

if new_resource.healthcheck_cmd
cmd << "--health-cmd #{new_resource.healthcheck_cmd}"
cmd << "--health-interval #{new_resource.healthcheck_interval}" if new_resource.healthcheck_interval
cmd << "--health-timeout #{new_resource.healthcheck_timeout}" if new_resource.healthcheck_timeout
cmd << "--health-retries #{new_resource.healthcheck_retries}" if new_resource.healthcheck_retries
end

cmd << new_resource.image
cmd << new_resource.command if new_resource.command
cmd
end

def update_service_cmd(new_resource)
cmd = %w(docker service update)
cmd << "--image #{new_resource.image}"
cmd << "--replicas #{new_resource.replicas}"
cmd << new_resource.service_name
cmd
end
end
45 changes: 45 additions & 0 deletions resources/swarm_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
unified_mode true

include DockerCookbook::DockerHelpers::Swarm

resource_name :docker_swarm_token
provides :docker_swarm_token

property :token_type, String, name_property: true, equal_to: %w(worker manager)
property :rotate, [true, false], default: false

load_current_value do |new_resource|
if swarm_manager?
cmd = Mixlib::ShellOut.new("docker swarm join-token -q #{new_resource.token_type}")
cmd.run_command
current_value_does_not_exist! if cmd.error?
else
current_value_does_not_exist!
end
end

action :read do
return unless swarm_manager?

converge_by "reading #{new_resource.token_type} token" do
cmd = Mixlib::ShellOut.new("docker swarm join-token -q #{new_resource.token_type}")
cmd.run_command
raise "Error getting #{new_resource.token_type} token: #{cmd.stderr}" if cmd.error?

node.run_state['docker_swarm'] ||= {}
node.run_state['docker_swarm']["#{new_resource.token_type}_token"] = cmd.stdout.strip
end
end

action :rotate do
return unless swarm_manager?

converge_by "rotating #{new_resource.token_type} token" do
cmd = Mixlib::ShellOut.new("docker swarm join-token --rotate -q #{new_resource.token_type}")
cmd.run_command
raise "Error rotating #{new_resource.token_type} token: #{cmd.stderr}" if cmd.error?

node.run_state['docker_swarm'] ||= {}
node.run_state['docker_swarm']["#{new_resource.token_type}_token"] = cmd.stdout.strip
end
end
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,9 @@ def self.reset!
config.before :each do
RSpecHelper.reset!
RSpecHelper.current_example = self
# Include DockerCookbook::DockerHelpers::Swarm in the example group
RSpec.configure do |c|
c.include DockerCookbook::DockerHelpers::Swarm
end
end
end
Loading

0 comments on commit b7d2cd2

Please sign in to comment.