All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.gesellix.docker.client.stack.DeployConfigReader.groovy Maven / Gradle / Ivy

There is a newer version: 2024-09-15T19-40-00-groovy-4
Show newest version
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.StackConfig
import de.gesellix.docker.client.stack.types.StackSecret
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.Limits
import de.gesellix.docker.compose.types.Logging
import de.gesellix.docker.compose.types.PlacementPreferences
import de.gesellix.docker.compose.types.PortConfigs
import de.gesellix.docker.compose.types.Reservations
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.StackNetwork
import de.gesellix.docker.compose.types.StackService
import de.gesellix.docker.compose.types.StackVolume
import de.gesellix.docker.compose.types.UpdateConfig
import de.gesellix.docker.remote.api.EndpointPortConfig
import de.gesellix.docker.remote.api.EndpointSpec
import de.gesellix.docker.remote.api.HealthConfig
import de.gesellix.docker.remote.api.IPAM
import de.gesellix.docker.remote.api.IPAMConfig
import de.gesellix.docker.remote.api.Limit
import de.gesellix.docker.remote.api.Mount
import de.gesellix.docker.remote.api.MountBindOptions
import de.gesellix.docker.remote.api.MountVolumeOptions
import de.gesellix.docker.remote.api.MountVolumeOptionsDriverConfig
import de.gesellix.docker.remote.api.Network
import de.gesellix.docker.remote.api.NetworkAttachmentConfig
import de.gesellix.docker.remote.api.NetworkCreateRequest
import de.gesellix.docker.remote.api.ResourceObject
import de.gesellix.docker.remote.api.ServiceSpec
import de.gesellix.docker.remote.api.ServiceSpecMode
import de.gesellix.docker.remote.api.ServiceSpecModeReplicated
import de.gesellix.docker.remote.api.ServiceSpecUpdateConfig
import de.gesellix.docker.remote.api.TaskSpec
import de.gesellix.docker.remote.api.TaskSpecContainerSpec
import de.gesellix.docker.remote.api.TaskSpecContainerSpecConfigsInner
import de.gesellix.docker.remote.api.TaskSpecContainerSpecConfigsInnerFile
import de.gesellix.docker.remote.api.TaskSpecContainerSpecSecretsInner
import de.gesellix.docker.remote.api.TaskSpecContainerSpecSecretsInnerFile
import de.gesellix.docker.remote.api.TaskSpecLogDriver
import de.gesellix.docker.remote.api.TaskSpecPlacement
import de.gesellix.docker.remote.api.TaskSpecPlacementPreferencesInner
import de.gesellix.docker.remote.api.TaskSpecPlacementPreferencesInnerSpread
import de.gesellix.docker.remote.api.TaskSpecResources
import de.gesellix.docker.remote.api.TaskSpecRestartPolicy
import org.slf4j.Logger
import org.slf4j.LoggerFactory

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.Matcher
import java.util.regex.Pattern

import static java.lang.Double.parseDouble
import static java.lang.Integer.parseInt
import static java.lang.Long.parseLong

class DeployConfigReader {

  private final Logger log = LoggerFactory.getLogger(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, StackService service ->
      if (!service.networks) {
        return ["default"]
      }
      return service.networks.collect { String networkName, ServiceNetwork serviceNetwork ->
        networkName
      }
    }.flatten().unique()
    log.info("service network names: ${serviceNetworkNames}")

    Map networkConfigs
    List externalNetworks
    (networkConfigs, externalNetworks) = networks(namespace, serviceNetworkNames, composeConfig.networks ?: [:])
    Map secrets = secrets(namespace, composeConfig.secrets, workingDir)
    Map configs = configs(namespace, composeConfig.configs, workingDir)
    Map services = services(namespace, composeConfig.services, composeConfig.networks, composeConfig.volumes)

    DeployStackConfig 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 { String name, StackService service ->
      Map serviceLabels = service.deploy?.labels?.entries ?: [:]
      serviceLabels[ManageStackClient.LabelNamespace] = namespace

      Map containerLabels = service.labels?.entries ?: [:]
      containerLabels[ManageStackClient.LabelNamespace] = namespace

      Long stopGracePeriod = null
      if (service.stopGracePeriod) {
        stopGracePeriod = parseDuration(service.stopGracePeriod).toNanos()
      }

      List env = convertEnvironment(service.workingDir, service.envFile, service.environment)
      Collections.sort(env)

      List extraHosts = convertExtraHosts(service.extraHosts)
      Collections.sort(extraHosts)

      ServiceSpec serviceConfig = new ServiceSpec()
      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 = new TaskSpec(
          null,
          new TaskSpecContainerSpec(
              service.image,
              containerLabels,
              service.entrypoint?.parts ?: [],
              service.command?.parts ?: [],
              service.hostname,
              env,
              service.workingDir,
              service.user,
              [],
              null,
              service.tty,
              service.stdinOpen,
              null,
              volumesToMounts(namespace, service.volumes as List, volumes),
              service.stopSignal,
              stopGracePeriod,
              convertHealthcheck(service.healthcheck),
              extraHosts,
              null,
              prepareServiceSecrets(namespace, service.secrets),
              prepareServiceConfigs(namespace, service.configs),
              null,
              null,
              [:],
              [],
              [],
              []
          ),
          null,
          serviceResources(service.deploy?.resources),
          restartPolicy(service.restart, service.deploy?.restartPolicy),
          new TaskSpecPlacement(
              service.deploy?.placement?.constraints,
              placementPreferences(service.deploy?.placement?.preferences),
              service.deploy?.maxReplicasPerNode,
              null
          ),
          null,
          null,
          [],
          logDriver(service.logging)
      )
      serviceSpec[name] = serviceConfig
    }
    log.info("services $serviceSpec")
    return serviceSpec
  }

  List prepareServiceConfigs(String namespace, List> configs) {
    configs?.collect { Map item ->
      if (item.size() > 1) {
        throw new RuntimeException("expected a unique config entry")
      }
      List converted = item.entrySet().collect { Map.Entry entry ->
        new TaskSpecContainerSpecConfigsInner(
            new TaskSpecContainerSpecConfigsInnerFile(
                entry.value?.target ?: (entry.value?.source ?: entry.key),
                entry.value?.uid ?: "0",
                entry.value?.gid ?: "0",
                entry.value?.mode ?: 0444
            ),
            null,
            "",
            "${namespace}_${entry.key}".toString()
        )
      }
      converted.first()
    } ?: []
  }

  List prepareServiceSecrets(String namespace, List> secrets) {
    secrets?.collect { Map item ->
      if (item.size() > 1) {
        throw new RuntimeException("expected a unique secret entry")
      }
      List converted = item.entrySet().collect { Map.Entry entry ->
        new TaskSpecContainerSpecSecretsInner(
            new TaskSpecContainerSpecSecretsInnerFile(
                entry.value?.target ?: (entry.value?.source ?: entry.key),
                entry.value?.uid ?: "0",
                entry.value?.gid ?: "0",
                entry.value?.mode ?: 0444
            ),
            "",
            "${namespace}_${entry.key}".toString()
        )
      }
      converted.first()
    } ?: []
  }

  List convertEnvironment(String workingDir, List envFiles, Environment environment) {
    List entries = []
    envFiles.each { String 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 { String name, String value ->
      return "${name}=${value}" as String
    } ?: [])
    return entries
  }

  List convertExtraHosts(ExtraHosts extraHosts) {
    extraHosts?.entries?.collect { String host, String ip ->
      "${host} ${ip}" as String
    } ?: []
  }

  ServiceSpecUpdateConfig convertUpdateConfig(UpdateConfig updateConfig) {
    if (!updateConfig) {
      return null
    }

    long parallel = 1
    if (updateConfig.parallelism) {
      parallel = updateConfig.parallelism
    }

    long delay = 0
    if (updateConfig.delay) {
      delay = parseDuration(updateConfig.delay).toNanos()
    }

    long monitor = 0
    if (updateConfig.monitor) {
      monitor = parseDuration(updateConfig.monitor).toNanos()
    }

    return new ServiceSpecUpdateConfig(
        parallel,
        delay,
        updateConfig.failureAction
            ? ServiceSpecUpdateConfig.FailureAction.values().find { ServiceSpecUpdateConfig.FailureAction action -> action.value == updateConfig.failureAction }
            : null,
        monitor,
        new BigDecimal(updateConfig.maxFailureRatio),
        updateConfig.order
            ? ServiceSpecUpdateConfig.Order.values().find { ServiceSpecUpdateConfig.Order order -> order.value == updateConfig.order }
            : null
    )
  }

  List convertServiceNetworks(
      Map serviceNetworks,
      Map networkConfigs,
      String namespace,
      String serviceName) {

    boolean isWindows = LocalDocker.isNativeWindows(dockerClient)

    if (serviceNetworks == null || serviceNetworks.isEmpty()) {
      serviceNetworks = ["default": null as ServiceNetwork]
    }

    List serviceNetworkConfigs = []

    serviceNetworks.each { String networkName, ServiceNetwork 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 << new NetworkAttachmentConfig(
          target,
          aliases,
          null
      )
    }

    Collections.sort(serviceNetworkConfigs, new NetworkConfigByTargetComparator())
    return serviceNetworkConfigs
  }

  String getTargetNetworkName(String namespace, String networkName, Map networkConfigs) {
    if (networkConfigs?.containsKey(networkName)) {
      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(NetworkAttachmentConfig o1, NetworkAttachmentConfig o2) {
      return o1.target <=> o2.target
    }
  }

  TaskSpecLogDriver logDriver(Logging logging) {
    if (logging) {
      return new TaskSpecLogDriver(
          logging.driver,
          logging.options
      )
    }
    return null
  }

  HealthConfig convertHealthcheck(Healthcheck healthcheck) {
    if (!healthcheck) {
      return null
    }

    Integer retries = null
    Long timeout = null
    Long interval = null
    Long startPeriod = null
    Long startInterval = null

    if (healthcheck.disable) {
      if (healthcheck.test?.parts) {
        throw new IllegalArgumentException("test and disable can't be set at the same time")
      }
      return new HealthConfig(["NONE"], null, null, null, null, null)
    }

    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()
    }
    if (healthcheck.startPeriod) {
      startPeriod = parseDuration(healthcheck.startPeriod).toNanos()
    }
    if (healthcheck.startInterval) {
      startInterval = parseDuration(healthcheck.startInterval).toNanos()
    }

    return new HealthConfig(
        healthcheck.test.parts,
        interval ?: 0,
        timeout ?: 0,
        retries,
        startPeriod,
        startInterval
    )
  }

  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 String numberWithUnitRegex = /(\d*)\.?(\d*)(\D+)/
  final Pattern pattern = Pattern.compile(numberWithUnitRegex)

  Duration parseDuration(String durationAsString) {
    String sign = '+'
    if (durationAsString.matches(/[-+].+/)) {
      sign = durationAsString.substring(0, '-'.length())
      durationAsString = durationAsString.substring('-'.length())
    }
    Matcher matcher = pattern.matcher(durationAsString)

    Duration duration = Duration.of(0, ChronoUnit.NANOS)
    boolean ok = false
    while (matcher.find()) {
      if (matcher.groupCount() != 3) {
        throw new IllegalStateException("expected 3 groups, but got ${matcher.groupCount()}")
      }
      String pre = matcher.group(1) ?: "0"
      String post = matcher.group(2) ?: "0"
      String symbol = matcher.group(3)
      if (!symbol) {
        throw new IllegalArgumentException("missing unit in duration '${durationAsString}'")
      }
      ChronoUnit unit = unitBySymbol[symbol]
      if (!unit) {
        throw new IllegalArgumentException("unknown unit ${symbol} in duration '${durationAsString}'")
      }

      double 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)
  }

  TaskSpecRestartPolicy restartPolicy(String restart, RestartPolicy restartPolicy) {
    // TODO: log if restart is being ignored
    if (restartPolicy == null) {
      Map policy = parseRestartPolicy(restart)
      if (!policy) {
        return null
      }
      switch (policy.name) {
        case "":
        case "no":
          return null

        case "always":
        case "unless-stopped":
          return new TaskSpecRestartPolicy(
              TaskSpecRestartPolicy.Condition.Any,
              null,
              null,
              null
          )

        case "on-failure":
          return new TaskSpecRestartPolicy(
              TaskSpecRestartPolicy.Condition.OnMinusFailure,
              null,
              policy.maximumRetryCount as int,
              null
          )

        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 new TaskSpecRestartPolicy(
          TaskSpecRestartPolicy.Condition.values().find { it.value == restartPolicy.condition },
          delay,
          restartPolicy.maxAttempts,
          window
      )
    }
  }

  Map parseRestartPolicy(String policy) {
    Map restartPolicy = [
        name: ""
    ]
    if (!policy) {
      return restartPolicy
    }

    String[] 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
  }

  TaskSpecResources serviceResources(Resources resources) {
    if (resources?.limits || resources?.reservations) {
      double nanoMultiplier = Math.pow(10, 9)
      return new TaskSpecResources(
          getTaskSpecResourcesLimits(resources?.limits, nanoMultiplier),
          getTaskSpecResourcesReservation(resources?.reservations, nanoMultiplier)
      )
    }
    return new TaskSpecResources()
  }

  Limit getTaskSpecResourcesLimits(Limits limits, double nanoMultiplier) {
    if (limits) {
      if (limits.nanoCpus) {
        if (limits.nanoCpus.contains('/')) {
          // TODO
          throw new UnsupportedOperationException("not supported, yet")
        } else {
          return new Limit(
              (parseDouble(limits.nanoCpus) * nanoMultiplier).longValue(),
              parseLong(limits.memory),
              null
          )
        }
      }
      return new Limit(
          null,
          parseLong(limits.memory),
          null
      )
    }
    return null
  }

  ResourceObject getTaskSpecResourcesReservation(Reservations reservations, double nanoMultiplier) {
    if (reservations) {
      if (reservations.nanoCpus) {
        if (reservations.nanoCpus.contains('/')) {
          // TODO
          throw new UnsupportedOperationException("not supported, yet")
        } else {
          return new ResourceObject(
              (parseDouble(reservations.nanoCpus) * nanoMultiplier).longValue(),
              parseLong(reservations.memory),
              null
          )
        }
      }
      return new ResourceObject(
          null,
          parseLong(reservations.memory),
          null
      )
    }
    return null
  }

  List volumesToMounts(String namespace, List serviceVolumes, Map stackVolumes) {
    List mounts = serviceVolumes.collect { serviceVolume ->
      return volumeToMount(namespace, serviceVolume, stackVolumes)
    }
    return mounts
  }

  Mount volumeToMount(String namespace, ServiceVolume volumeSpec, Map stackVolumes) {
    if (volumeSpec.source == "") {
      // Anonymous volume
      return new Mount(
          volumeSpec.target,
          null,
          Mount.Type.values().find { it.getValue() == volumeSpec.type },
          null,
          null,
          null,
          null,
          null
      )
    }

    if (volumeSpec.type == ServiceVolumeType.TypeBind.typeName) {
      return new Mount(
          volumeSpec.target,
          volumeSpec.source,
          Mount.Type.Bind,
          volumeSpec.readOnly,
          null,
          getBindOptions(volumeSpec.bind),
          null,
          null)
    }

    if (!stackVolumes.containsKey(volumeSpec.source)) {
      throw new IllegalArgumentException("undefined volume: ${volumeSpec.source}")
    }
    StackVolume stackVolume = stackVolumes[volumeSpec.source]

    String source = volumeSpec.source
    MountVolumeOptions volumeOptions
    if (stackVolume?.external?.name) {
      volumeOptions = new MountVolumeOptions(
          volumeSpec.volume?.nocopy ?: false,
          null,
          null)
      source = stackVolume.external.name
    } else {
      Map labels = stackVolume?.labels?.entries ?: [:]
      labels[(ManageStackClient.LabelNamespace)] = namespace
      volumeOptions = new MountVolumeOptions(
          volumeSpec.volume?.nocopy ?: false,
          labels,
          getMountVolumeOptionsDriverConfig(stackVolume)
      )
      source = "${namespace}_${volumeSpec.source}" as String
      if (stackVolume?.name) {
        source = stackVolume.name
      }
    }

    return new Mount(
        volumeSpec.target,
        source,
        Mount.Type.Volume,
        volumeSpec.readOnly,
        null,
        null,
        volumeOptions,
        null
    )
  }

  boolean isReadOnly(List modes) {
    return modes.contains("ro")
  }

  boolean isNoCopy(List modes) {
    return modes.contains("nocopy")
  }

  MountVolumeOptionsDriverConfig getMountVolumeOptionsDriverConfig(StackVolume stackVolume) {
    if (stackVolume?.driver && stackVolume?.driver != "") {
      return new MountVolumeOptionsDriverConfig(
          stackVolume.driver,
          stackVolume.driverOpts.options
      )
    }
    return null
  }

  MountBindOptions getBindOptions(ServiceVolumeBind bind) {
    if (bind?.propagation) {
      return new MountBindOptions(MountBindOptions.Propagation.values().find { MountBindOptions.Propagation propagation -> propagation.getValue() == bind.propagation }, null)
    } else {
      return null
    }
  }

  EndpointSpec serviceEndpoints(String endpointMode, PortConfigs portConfigs) {
    EndpointSpec endpointSpec = new EndpointSpec(
        endpointMode ? EndpointSpec.Mode.values().find { it.value == endpointMode } : EndpointSpec.Mode.Vip,
        portConfigs.portConfigs.collect { portConfig ->
          new EndpointPortConfig(
              null,
              portConfig.protocol ? EndpointPortConfig.Protocol.values().find { it.value == portConfig.protocol } : null,
              portConfig.target,
              portConfig.published,
              portConfig.mode ? EndpointPortConfig.PublishMode.values().find { it.value == portConfig.mode } : null
          )
        }
    )
    return endpointSpec
  }

  ServiceSpecMode serviceMode(String mode, Integer replicas) {
    switch (mode) {
      case "global":
        if (replicas) {
          throw new IllegalArgumentException("replicas can only be used with replicated mode")
        }
        return new ServiceSpecMode(null, [:], null, null)

      case null:
      case "":
      case "replicated":
        return new ServiceSpecMode(new ServiceSpecModeReplicated(replicas ?: 1), null, null, null)

      default:
        throw new IllegalArgumentException("Unknown mode: '$mode'")
    }
  }

  Tuple2, List> networks(
      String namespace,
      List serviceNetworkNames,
      Map networks) {
    Map networkSpec = [:]

    List externalNetworkNames = []
    serviceNetworkNames.each { String internalName ->
      StackNetwork network = networks[internalName]
      if (!network) {
        networkSpec[internalName] = new NetworkCreateRequest(
            internalName,
            true,
            "overlay",
            null, null,
            null, null, null,
            null,
            getLabels(namespace, null)
        )
      } else if (network?.external?.external) {
        externalNetworkNames << (network.external.name ?: internalName)
      } else {
        networkSpec[internalName] = new NetworkCreateRequest(
            internalName,
            true,
            network.driver ?: "overlay",
            Boolean.valueOf(network.internal),
            network.attachable,
            null,
            getIpam(network),
            null,
            network.driverOpts.options,
            getLabels(namespace, network)
        )
      }
    }

    log.info("network configs: ${networkSpec}")
    log.info("external networks: ${externalNetworkNames}")

    validateExternalNetworks(externalNetworkNames)

    return [networkSpec, externalNetworkNames]
  }

  IPAM getIpam(StackNetwork network) {
    if (!network) {
      return null
    }
    if (!network.ipam?.driver && !network.ipam?.config) {
      return null
    }
    List ipamConfig = []
    if (network.ipam?.config) {
      network.ipam.config.each { IpamConfig config ->
        ipamConfig << new IPAMConfig().tap {
          subnet = config.subnet
        }
      }
    }
    return new IPAM(
        network.ipam?.driver,
        ipamConfig,
        null
    )
  }

  Map getLabels(String namespace, StackNetwork network) {
    Map labels = [:]
    labels.putAll(network?.labels?.entries ?: [:])
    labels[(ManageStackClient.LabelNamespace)] = namespace
    return labels
  }

  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))
  }

  void 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 ->
      Network network
      try {
        network = dockerClient.inspectNetwork(name).content
      }
      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.scope != "swarm") {
        log.error("network ${name} is declared as external, but it is not in the right scope: '${network.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)

        Map 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)

        Map 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
    }
    TaskSpecPlacementPreferencesInnerSpread spread = new TaskSpecPlacementPreferencesInnerSpread(preferences[0].spread)
    log.info("spread: ${spread}")
    return [new TaskSpecPlacementPreferencesInner(spread)]
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy