Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Docker Swarm support #1296

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ jobs:
suite: ${{ matrix.suite }}
os: ${{ matrix.os }}

integration-swarm:
needs: lint-unit
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Install Chef
uses: actionshub/[email protected]
- name: Install Docker
uses: docker/setup-docker-action@v4
- name: Test Kitchen
uses: actionshub/[email protected]
env:
CHEF_VERSION: latest
CHEF_LICENSE: accept-no-persist
KITCHEN_LOCAL_YAML: kitchen.exec.yml
with:
suite: swarm
os: ubuntu-latest

integration-smoke:
needs: lint-unit
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruby system
26 changes: 24 additions & 2 deletions kitchen.exec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,27 @@ driver: { name: exec }
transport: { name: exec }

platforms:
- name: macos-latest
- name: windows-latest
- name: ubuntu-latest

suites:
- name: swarm
provisioner:
enforce_idempotency: false
multiple_converge: 1
attributes:
docker:
version: '20.10.11'
swarm:
init:
advertise_addr: '127.0.0.1'
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]
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?
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?
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?
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?

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?

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?

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

action :leave do
return unless swarm_member?

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?

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?
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?
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
Loading
Loading