Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
de.gesellix.docker.client.stack.DeployConfigReader.groovy Maven / Gradle / Ivy
package de.gesellix.docker.client.stack
import de.gesellix.docker.client.DockerClient
import de.gesellix.docker.client.EnvFileParser
import de.gesellix.docker.client.LocalDocker
import de.gesellix.docker.client.stack.types.PlacementPreferences
import de.gesellix.docker.client.stack.types.ResolutionMode
import de.gesellix.docker.client.stack.types.RestartPolicyCondition
import de.gesellix.docker.client.stack.types.Spread
import de.gesellix.docker.client.stack.types.StackConfig
import de.gesellix.docker.client.stack.types.StackNetwork
import de.gesellix.docker.client.stack.types.StackSecret
import de.gesellix.docker.client.stack.types.StackService
import de.gesellix.docker.compose.ComposeFileReader
import de.gesellix.docker.compose.types.ComposeConfig
import de.gesellix.docker.compose.types.Environment
import de.gesellix.docker.compose.types.ExtraHosts
import de.gesellix.docker.compose.types.Healthcheck
import de.gesellix.docker.compose.types.IpamConfig
import de.gesellix.docker.compose.types.Logging
import de.gesellix.docker.compose.types.PortConfigs
import de.gesellix.docker.compose.types.Resources
import de.gesellix.docker.compose.types.RestartPolicy
import de.gesellix.docker.compose.types.ServiceConfig
import de.gesellix.docker.compose.types.ServiceNetwork
import de.gesellix.docker.compose.types.ServiceSecret
import de.gesellix.docker.compose.types.ServiceVolume
import de.gesellix.docker.compose.types.ServiceVolumeBind
import de.gesellix.docker.compose.types.ServiceVolumeType
import de.gesellix.docker.compose.types.StackVolume
import de.gesellix.docker.compose.types.UpdateConfig
import groovy.util.logging.Slf4j
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.regex.Pattern
import static de.gesellix.docker.client.stack.types.ResolutionMode.ResolutionModeVIP
import static de.gesellix.docker.client.stack.types.RestartPolicyCondition.RestartPolicyConditionAny
import static de.gesellix.docker.client.stack.types.RestartPolicyCondition.RestartPolicyConditionOnFailure
import static de.gesellix.docker.compose.types.ServiceVolumeType.TypeVolume
import static java.lang.Double.parseDouble
import static java.lang.Integer.parseInt
import static java.lang.Long.parseLong
@Slf4j
class DeployConfigReader {
DockerClient dockerClient
ComposeFileReader composeFileReader = new ComposeFileReader()
DeployConfigReader(DockerClient dockerClient) {
this.dockerClient = dockerClient
}
@Deprecated
DeployStackConfig loadCompose(String namespace, InputStream composeFile, String workingDir) {
loadCompose(namespace, composeFile, workingDir, System.getenv())
}
// TODO test me
DeployStackConfig loadCompose(String namespace, InputStream composeFile, String workingDir, Map environment) {
ComposeConfig composeConfig = composeFileReader.load(composeFile, workingDir, environment)
log.info("composeContent: $composeConfig}")
List serviceNetworkNames = composeConfig.services.collect { String name, de.gesellix.docker.compose.types.StackService service ->
if (!service.networks) {
return ["default"]
}
return service.networks.collect { String networkName, serviceNetwork ->
networkName
}
}.flatten().unique()
log.info("service network names: ${serviceNetworkNames}")
Map networkConfigs
List externalNetworks
(networkConfigs, externalNetworks) = networks(namespace, serviceNetworkNames, composeConfig.networks ?: [:])
def secrets = secrets(namespace, composeConfig.secrets, workingDir)
def configs = configs(namespace, composeConfig.configs, workingDir)
def services = services(namespace, composeConfig.services, composeConfig.networks, composeConfig.volumes)
def cfg = new DeployStackConfig()
cfg.networks = networkConfigs
cfg.secrets = secrets
cfg.configs = configs
cfg.services = services
return cfg
}
Map services(
String namespace,
Map services,
Map networks,
Map volumes) {
Map serviceSpec = [:]
services.each { name, service ->
def serviceLabels = service.deploy?.labels?.entries ?: [:]
serviceLabels[ManageStackClient.LabelNamespace] = namespace
def containerLabels = service.labels?.entries ?: [:]
containerLabels[ManageStackClient.LabelNamespace] = namespace
Long stopGracePeriod = null
if (service.stopGracePeriod) {
stopGracePeriod = parseDuration(service.stopGracePeriod).toNanos()
}
def env = convertEnvironment(service.workingDir, service.envFile, service.environment)
Collections.sort(env)
def extraHosts = convertExtraHosts(service.extraHosts)
Collections.sort(extraHosts)
def serviceConfig = new StackService()
serviceConfig.name = ("${namespace}_${name}" as String)
serviceConfig.labels = serviceLabels
serviceConfig.endpointSpec = serviceEndpoints(service.deploy?.endpointMode, service.ports)
serviceConfig.mode = serviceMode(service.deploy?.mode, service.deploy?.replicas)
serviceConfig.networks = convertServiceNetworks(service.networks ?: [:], networks, namespace, name)
serviceConfig.updateConfig = convertUpdateConfig(service.deploy?.updateConfig)
serviceConfig.taskTemplate = [
containerSpec: [
image : service.image,
command : service.entrypoint?.parts ?: [],
args : service.command?.parts ?: [],
hostname : service.hostname,
hosts : extraHosts,
healthcheck : convertHealthcheck(service.healthcheck),
env : env,
labels : containerLabels,
dir : service.workingDir,
user : service.user,
mounts : volumesToMounts(namespace, service.volumes as List, volumes),
stopGracePeriod: stopGracePeriod,
stopSignal : service.stopSignal,
tty : service.tty,
openStdin : service.stdinOpen,
configs : prepareServiceConfigs(namespace, service.configs),
secrets : prepareServiceSecrets(namespace, service.secrets),
],
logDriver : logDriver(service.logging),
resources : serviceResources(service.deploy?.resources),
restartPolicy: restartPolicy(service.restart, service.deploy?.restartPolicy),
placement : [
constraints: service.deploy?.placement?.constraints,
preferences: placementPreferences(service.deploy?.placement?.preferences),
maxReplicas: service.deploy?.maxReplicasPerNode
],
]
serviceSpec[name] = serviceConfig
}
log.info("services $serviceSpec")
return serviceSpec
}
List prepareServiceConfigs(String namespace, List> configs) {
configs?.collect { item ->
if (item.size() > 1) {
throw new RuntimeException("expected a unique config entry")
}
def converted = item.entrySet().collect { entry ->
[
File : [Name: entry.value?.target ?: (entry.value?.source ?: entry.key),
UID : entry.value?.uid ?: "0",
GID : entry.value?.gid ?: "0",
Mode: entry.value?.mode ?: 0444],
ConfigID : "",
ConfigName: "${namespace}_${entry.key}" as String
]
}
converted.first()
} ?: []
}
List prepareServiceSecrets(String namespace, List> secrets) {
secrets?.collect { item ->
if (item.size() > 1) {
throw new RuntimeException("expected a unique secret entry")
}
def converted = item.entrySet().collect { entry ->
[
File : [Name: entry.value?.target ?: (entry.value?.source ?: entry.key),
UID : entry.value?.uid ?: "0",
GID : entry.value?.gid ?: "0",
Mode: entry.value?.mode ?: 0444],
SecretID : "",
SecretName: "${namespace}_${entry.key}" as String
]
}
converted.first()
} ?: []
}
List convertEnvironment(String workingDir, List envFiles, Environment environment) {
List entries = []
envFiles.each { filename ->
File file = new File(filename)
if (!file.isAbsolute()) {
file = new File(workingDir, filename)
}
entries.addAll(new EnvFileParser().parse(file))
}
entries.addAll(environment?.entries?.collect { name, value ->
return "${name}=${value}" as String
} ?: [])
return entries
}
List convertExtraHosts(ExtraHosts extraHosts) {
extraHosts?.entries?.collect { host, ip ->
"${host} ${ip}" as String
} ?: []
}
def convertUpdateConfig(UpdateConfig updateConfig) {
if (!updateConfig) {
return null
}
def parallel = 1
if (updateConfig.parallelism) {
parallel = updateConfig.parallelism
}
def delay = 0
if (updateConfig.delay) {
delay = parseDuration(updateConfig.delay).toNanos()
}
def monitor = 0
if (updateConfig.monitor) {
monitor = parseDuration(updateConfig.monitor).toNanos()
}
return [
parallelism : parallel,
delay : delay,
failureAction : updateConfig.failureAction,
monitor : monitor,
maxFailureRatio: updateConfig.maxFailureRatio,
order : updateConfig.order
]
}
def convertServiceNetworks(
Map serviceNetworks,
Map networkConfigs,
String namespace,
String serviceName) {
boolean isWindows = LocalDocker.isNativeWindows(dockerClient)
if (serviceNetworks == null || serviceNetworks.isEmpty()) {
serviceNetworks = ["default": null as ServiceNetwork]
}
def serviceNetworkConfigs = []
serviceNetworks.each { networkName, serviceNetwork ->
if (!networkConfigs?.containsKey(networkName) && networkName != "default") {
throw new IllegalStateException("service ${serviceName} references network ${networkName}, which is not declared")
}
List aliases = []
if (serviceNetwork) {
aliases = serviceNetwork.aliases
}
String target = getTargetNetworkName(namespace, networkName, networkConfigs)
if (isUserDefined(target, isWindows)) {
aliases << serviceName
}
serviceNetworkConfigs << [
target : target,
aliases: aliases,
]
}
Collections.sort(serviceNetworkConfigs, new NetworkConfigByTargetComparator())
return serviceNetworkConfigs
}
String getTargetNetworkName(String namespace, String networkName, Map networkConfigs) {
if (networkConfigs?.containsKey(networkName)) {
de.gesellix.docker.compose.types.StackNetwork networkConfig = networkConfigs[networkName]
if (networkConfig?.external?.external) {
if (networkConfig?.external?.name) {
return networkConfig.external.name
}
else {
return networkName
}
}
}
return "${namespace}_${networkName}" as String
}
static class NetworkConfigByTargetComparator implements Comparator {
@Override
int compare(Object o1, Object o2) {
return o1.target.compareTo(o2.target)
}
}
def logDriver(Logging logging) {
if (logging) {
return [
name : logging.driver,
options: logging.options,
]
}
return null
}
def convertHealthcheck(Healthcheck healthcheck) {
if (!healthcheck) {
return null
}
Integer retries = null
Long timeout = null
Long interval = null
if (healthcheck.disable) {
if (healthcheck.test?.parts) {
throw new IllegalArgumentException("test and disable can't be set at the same time")
}
return [
test: ["NONE"]
]
}
if (healthcheck.timeout) {
timeout = parseDuration(healthcheck.timeout).toNanos()
}
if (healthcheck.interval) {
interval = parseDuration(healthcheck.interval).toNanos()
}
if (healthcheck.retries) {
retries = new Float(healthcheck.retries).intValue()
}
return [
test : healthcheck.test.parts,
timeout : timeout ?: 0,
interval: interval ?: 0,
retries : retries,
]
}
Map unitBySymbol = [
"ns": ChronoUnit.NANOS,
"us": ChronoUnit.MICROS,
"µs": ChronoUnit.MICROS, // U+00B5 = micro symbol
"μs": ChronoUnit.MICROS, // U+03BC = Greek letter mu
"ms": ChronoUnit.MILLIS,
"s" : ChronoUnit.SECONDS,
"m" : ChronoUnit.MINUTES,
"h" : ChronoUnit.HOURS
]
final def numberWithUnitRegex = /(\d*)\.?(\d*)(\D+)/
final def pattern = Pattern.compile(numberWithUnitRegex)
def parseDuration(String durationAsString) {
def sign = '+'
if (durationAsString.matches(/[-+].+/)) {
sign = durationAsString.substring(0, '-'.length())
durationAsString = durationAsString.substring('-'.length())
}
def matcher = pattern.matcher(durationAsString)
def duration = Duration.of(0, ChronoUnit.NANOS)
def ok = false
while (matcher.find()) {
if (matcher.groupCount() != 3) {
throw new IllegalStateException("expected 3 groups, but got ${matcher.groupCount()}")
}
def pre = matcher.group(1) ?: "0"
def post = matcher.group(2) ?: "0"
def symbol = matcher.group(3)
if (!symbol) {
throw new IllegalArgumentException("missing unit in duration '${durationAsString}'")
}
def unit = unitBySymbol[symbol]
if (!unit) {
throw new IllegalArgumentException("unknown unit ${symbol} in duration '${durationAsString}'")
}
def scale = Math.pow(10, post.length())
duration = duration
.plus(parseInt(pre), unit)
.plus((int) (parseInt(post) * (unit.duration.nano / scale)), ChronoUnit.NANOS)
ok = true
}
if (!ok) {
throw new IllegalStateException("duration couldn't be parsed: '${durationAsString}'")
}
return duration.multipliedBy(sign == '-' ? -1 : 1)
}
def restartPolicy(String restart, RestartPolicy restartPolicy) {
// TODO: log if restart is being ignored
if (restartPolicy == null) {
def policy = parseRestartPolicy(restart)
if (!policy) {
return null
}
switch (policy.name) {
case "":
case "no":
return null
case "always":
case "unless-stopped":
return [
condition: RestartPolicyConditionAny.value
]
case "on-failure":
return [
condition : RestartPolicyConditionOnFailure.value,
maxAttempts: policy.maximumRetryCount
]
default:
throw new IllegalArgumentException("unknown restart policy: ${restart}")
}
}
else {
Long delay = null
if (restartPolicy.delay) {
delay = parseDuration(restartPolicy.delay).toNanos()
}
Long window = null
if (restartPolicy.window) {
window = parseDuration(restartPolicy.window).toNanos()
}
return [
condition : RestartPolicyCondition.byValue(restartPolicy.condition).value,
delay : delay,
maxAttempts: restartPolicy.maxAttempts,
window : window,
]
}
}
def parseRestartPolicy(String policy) {
def restartPolicy = [
name: ""
]
if (!policy) {
return restartPolicy
}
def parts = policy.split(':')
if (parts.length > 2) {
throw new IllegalArgumentException("invalid restart policy format: '${policy}")
}
if (parts.length == 2) {
if (!parts[1].isInteger()) {
throw new IllegalArgumentException("maximum retry count must be an integer")
}
restartPolicy.maximumRetryCount = parseInt(parts[1])
}
restartPolicy.name = parts[0]
return restartPolicy
}
def serviceResources(Resources resources) {
def resourceRequirements = [:]
def nanoMultiplier = Math.pow(10, 9)
if (resources?.limits) {
resourceRequirements['limits'] = [:]
if (resources.limits.nanoCpus) {
if (resources.limits.nanoCpus.contains('/')) {
// TODO
throw new UnsupportedOperationException("not supported, yet")
}
else {
resourceRequirements['limits'].nanoCPUs = parseDouble(resources.limits.nanoCpus) * nanoMultiplier
}
}
resourceRequirements['limits'].memoryBytes = parseLong(resources.limits.memory)
}
if (resources?.reservations) {
resourceRequirements['reservations'] = [:]
if (resources.reservations.nanoCpus) {
if (resources.reservations.nanoCpus.contains('/')) {
// TODO
throw new UnsupportedOperationException("not supported, yet")
}
else {
resourceRequirements['reservations'].nanoCPUs = parseDouble(resources.reservations.nanoCpus) * nanoMultiplier
}
}
resourceRequirements['reservations'].memoryBytes = parseLong(resources.reservations.memory)
}
return resourceRequirements
}
def volumesToMounts(String namespace, List serviceVolumes, Map stackVolumes) {
def mounts = serviceVolumes.collect { serviceVolume ->
return volumeToMount(namespace, serviceVolume, stackVolumes)
}
return mounts
}
Map volumeToMount(String namespace, ServiceVolume volumeSpec, Map stackVolumes) {
if (volumeSpec.source == "") {
// Anonymous volume
return [
type : volumeSpec.type,
target: volumeSpec.target,
]
}
if (volumeSpec.type == ServiceVolumeType.TypeBind.typeName) {
return [
type : volumeSpec.type,
source : volumeSpec.source,
target : volumeSpec.target,
readOnly : volumeSpec.readOnly,
bindOptions: getBindOptions(volumeSpec.bind)
]
}
if (!stackVolumes.containsKey(volumeSpec.source)) {
throw new IllegalArgumentException("undefined volume: ${volumeSpec.source}")
}
def stackVolume = stackVolumes[volumeSpec.source]
String source = volumeSpec.source
def volumeOptions
if (stackVolume?.external?.name) {
volumeOptions = [
noCopy: volumeSpec.volume?.noCopy ?: false,
]
source = stackVolume.external.name
}
else {
def labels = stackVolume?.labels?.entries ?: [:]
labels[(ManageStackClient.LabelNamespace)] = namespace
volumeOptions = [
labels: labels,
noCopy: volumeSpec.volume?.noCopy ?: false,
]
if (stackVolume?.driver && stackVolume?.driver != "") {
volumeOptions.driverConfig = [
name : stackVolume.driver,
options: stackVolume.driverOpts.options
]
}
source = "${namespace}_${volumeSpec.source}" as String
if (stackVolume?.name) {
source = stackVolume.name
}
}
return [
type : TypeVolume.typeName,
target : volumeSpec.target,
source : source,
readOnly : volumeSpec.readOnly,
volumeOptions: volumeOptions
]
}
boolean isReadOnly(List modes) {
return modes.contains("ro")
}
boolean isNoCopy(List modes) {
return modes.contains("nocopy")
}
def getBindOptions(ServiceVolumeBind bind) {
if (bind?.propagation) {
return [propagation: bind.propagation]
}
else {
return null
}
}
def serviceEndpoints(String endpointMode, PortConfigs portConfigs) {
def endpointSpec = [
mode : endpointMode ? ResolutionMode.byValue(endpointMode).value : ResolutionModeVIP.value,
ports: portConfigs.portConfigs.collect { portConfig ->
[
protocol : portConfig.protocol,
targetPort : portConfig.target,
publishedPort: portConfig.published,
publishMode : portConfig.mode,
]
}
]
return endpointSpec
}
def serviceMode(String mode, Integer replicas) {
switch (mode) {
case "global":
if (replicas) {
throw new IllegalArgumentException("replicas can only be used with replicated mode")
}
return [global: [:]]
case null:
case "":
case "replicated":
return [replicated: [replicas: replicas ?: 1]]
default:
throw new IllegalArgumentException("Unknown mode: '$mode'")
}
}
Tuple2, List> networks(
String namespace,
List serviceNetworkNames,
Map networks) {
Map networkSpec = [:]
def externalNetworkNames = []
serviceNetworkNames.each { String internalName ->
def network = networks[internalName]
if (!network) {
def createOpts = new StackNetwork()
createOpts.labels = [(ManageStackClient.LabelNamespace): namespace]
createOpts.driver = "overlay"
networkSpec[internalName] = createOpts
}
else if (network?.external?.external) {
externalNetworkNames << (network.external.name ?: internalName)
}
else {
def createOpts = new StackNetwork()
Map labels = [:]
labels.putAll(network.labels?.entries ?: [:])
labels[(ManageStackClient.LabelNamespace)] = namespace
createOpts.labels = labels
createOpts.driver = network.driver ?: "overlay"
createOpts.driverOpts = network.driverOpts.options
createOpts.internal = Boolean.valueOf(network.internal)
createOpts.attachable = network.attachable
if (network.ipam?.driver || network.ipam?.config) {
createOpts.ipam = [:]
}
if (network.ipam?.driver) {
createOpts.ipam.driver = network.ipam.driver
}
if (network.ipam?.config) {
createOpts.ipam.config = []
network.ipam.config.each { IpamConfig config ->
createOpts.ipam.config << [subnet: config.subnet]
}
}
networkSpec[internalName] = createOpts
}
}
log.info("network configs: ${networkSpec}")
log.info("external networks: ${externalNetworkNames}")
validateExternalNetworks(externalNetworkNames)
return [networkSpec, externalNetworkNames]
}
boolean isContainerNetwork(String networkName) {
String[] elements = networkName?.split(':', 2)
return elements?.size() > 1 && elements[0] == "container"
}
boolean isUserDefined(String networkName, boolean isWindows) {
List blacklist = isWindows ? ["default", "none", "nat"] : ["default", "bridge", "host", "none"]
return !(networkName in blacklist || isContainerNetwork(networkName))
}
def validateExternalNetworks(List externalNetworks) {
boolean isWindows = LocalDocker.isNativeWindows(dockerClient)
externalNetworks.findAll { name ->
// Networks that are not user defined always exist on all nodes as
// local-scoped networks, so there's no need to inspect them.
isUserDefined(name, isWindows)
}.each { name ->
def network
try {
network = dockerClient.inspectNetwork(name)
}
catch (Exception e) {
log.error("network ${name} is declared as external, but could not be inspected. You need to create the network before the stack is deployed (with overlay driver)")
throw new IllegalStateException("network ${name} is declared as external, but could not be inspected.", e)
}
if (network.content.Scope != "swarm") {
log.error("network ${name} is declared as external, but it is not in the right scope: '${network.content.Scope}' instead of 'swarm'")
throw new IllegalStateException("network ${name} is declared as external, but is not in 'swarm' scope.")
}
}
}
Map secrets(String namespace, Map secrets, String workingDir) {
Map secretSpec = [:]
secrets.each { name, secret ->
if (!secret.external.external) {
Path filePath = Paths.get(workingDir, secret.file)
byte[] data = Files.readAllBytes(filePath)
def labels = new HashMap()
if (secret.labels?.entries) {
labels.putAll(secret.labels.entries)
}
labels[ManageStackClient.LabelNamespace] = namespace
secretSpec[name] = new StackSecret(
name: ("${namespace}_${name}" as String),
data: data,
labels: labels
)
}
}
log.info("secrets ${secretSpec.keySet()}")
return secretSpec
}
Map configs(String namespace, Map configs, String workingDir) {
Map configSpec = [:]
configs.each { name, config ->
if (!config.external.external) {
Path filePath = Paths.get(workingDir, config.file)
byte[] data = Files.readAllBytes(filePath)
def labels = new HashMap()
if (config.labels?.entries) {
labels.putAll(config.labels.entries)
}
labels[ManageStackClient.LabelNamespace] = namespace
configSpec[name] = new StackConfig(
name: ("${namespace}_${name}" as String),
data: data,
labels: labels
)
}
}
log.info("config ${configSpec.keySet()}")
return configSpec
}
List placementPreferences(List preferences) {
log.info("placementPreferences: ${preferences}")
if (preferences == null) {
return null
}
def spread = new Spread(spreadDescriptor: preferences[0].spread)
log.info("spread: ${spread}")
return [new PlacementPreferences(spread: spread)]
}
}