Created
October 29, 2024 11:35
-
-
Save kriskbx/fcfe9b11f1604ee267f70dcc62254e33 to your computer and use it in GitHub Desktop.
Static ip-addresses for Docker Swarm services
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* This is a node.js script supposed to run regularly on a docker swarm node and allows | |
* containers in a macvlan (or ipvlan) network to have static ip addresses. This is currently | |
* not possible using Docker Swarm. It doesn't use any external dependencies and instead | |
* calls docker cli commands directly. So, if you want to run it in a container, make | |
* sure to bind the docker socket into the container as well. | |
* | |
* It works by reading a `macvlan-static-ip` label from the service, e.g. 192.168.0.80 | |
* (This is the desired ip address for the resulting container). And then it "starts" | |
* and "stops" containers in the right order to ensure they get their configured ip. | |
* | |
* This means, your desired ip must be at the start of the configured ip-range of your | |
* macvlan/ipvlan network and there mustn't be any gaps between ip-addresses. | |
* | |
* Example: | |
* | |
* Let's say you configured this subnet for your macvlan: 192.168.0.240/29 | |
* This includes ip-addresses from 192.168.0.241 to 192.168.0.246 | |
* If you have 3 containers, they must use the following ips: 192.168.0.241, 192.168.0.242 | |
* and 192.168.0.243. You cannot use *.241, *.244 and *.246, there cannot be any gaps and | |
* they must start at the beginning. | |
*/ | |
const {execSync} = require('child_process'); | |
const macvlanNetwork = 'macvlan-network'; | |
const macvlanLabel = 'macvlan-static-ip'; | |
function docker(command, multiple = false) { | |
const result = (execSync(`docker ${command}`) + '').trim(); | |
if (multiple) { | |
return result.split('\n').filter(a => a); | |
} else { | |
return result; | |
} | |
} | |
function getContainer(serviceId) { | |
return docker(`service ps ${serviceId} --format '{{ .ID }}'`, true) | |
.map(taskId => { | |
const id = docker(`inspect ${taskId} --format '{{ .Status.ContainerStatus.ContainerID }}'`); | |
try { | |
const state = docker(`inspect ${id} --format '{{ .State.Status }}' 2>/dev/null`); | |
return {id, state}; | |
} catch (e) { | |
return null; | |
} | |
}) | |
.filter(c => c); | |
} | |
console.info('--------------------------------------'); | |
console.info('Starting process…'); | |
const services = docker(`service ls --filter label=${macvlanLabel} -q`, true) | |
.map(serviceId => { | |
const name = docker(`service inspect ${serviceId} --format "{{ .Spec.Name }}"`); | |
const ipWanted = docker(`service inspect ${serviceId} --format '{{ $label := index .Spec.Labels "${macvlanLabel}" }}{{ $label }}'`); | |
console.info(`Detected service ${name} with requested ip ${ipWanted}:`); | |
console.info(`| - Querying container for ${name}...`); | |
let container = getContainer(serviceId); | |
if (container.filter(c => c.state === "running").length === 0) { | |
console.info(`| - No running container found for service ${name}. Restarting service...`); | |
docker(`service scale ${name}=1`); | |
docker(`service update --force ${name}`); | |
container = getContainer(serviceId); | |
} | |
if (container.filter(c => c.state === "running").length === 0) { | |
console.error(`| - Still no running container found for service ${name}. Exiting...`); | |
process.exit(); | |
} | |
console.log(`| - ${container.length} container found...`) | |
return {name, ipWanted, container}; | |
}); | |
console.info('Looking for missmatched macvlan ips...'); | |
const missmatchedServices = docker(`network inspect ${macvlanNetwork} --format '{{ range $k, $v := .Containers }}{{ $all := print $k "," $v.IPv4Address }}{{ println $all }}{{ end }}'`, true) | |
.map(data => { | |
const [containerId, containerIp] = data.split(','); | |
const actualIp = containerIp.replace('/24', ''); | |
console.info(`| - Found ${containerId} with ip ${actualIp}`) | |
const service = services.find(service => | |
service.container.findIndex(container => | |
container.id === containerId && | |
service.ipWanted !== actualIp | |
) !== -1 | |
); | |
if (!service) { | |
console.info(`| - Ip is matching the required one. Continuing...`); | |
return null; | |
} | |
console.info(`| - Ip missmatch detected for service ${service.name}. Actual: ${actualIp} Wanted: ${service.ipWanted}`); | |
return { | |
...service, | |
actualIp, | |
}; | |
}) | |
.filter(m => m) | |
.sort((a, b) => { | |
return a.ipWanted > b.ipWanted ? 1 : -1; | |
}); | |
missmatchedServices | |
.forEach(service => { | |
console.info(`Shutting down service ${service.name}...`); | |
docker(`service scale ${service.name}=0`); | |
}); | |
missmatchedServices.forEach(service => { | |
console.info(`Starting service ${service.name}...`); | |
docker(`service scale ${service.name}=1`); | |
}); | |
if (missmatchedServices.length === 0) { | |
console.info('No ip missmatches detected. Exiting...') | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment