de.gesellix.docker.client.stack.ManageStackClient.groovy Maven / Gradle / Ivy
package de.gesellix.docker.client.stack
import de.gesellix.docker.client.DockerResponseHandler
import de.gesellix.docker.client.authentication.ManageAuthentication
import de.gesellix.docker.client.config.ManageConfig
import de.gesellix.docker.client.network.ManageNetwork
import de.gesellix.docker.client.node.ManageNode
import de.gesellix.docker.client.secret.ManageSecret
import de.gesellix.docker.client.service.ManageService
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.client.system.ManageSystem
import de.gesellix.docker.client.tasks.ManageTask
import de.gesellix.docker.engine.EngineClient
import de.gesellix.docker.engine.EngineResponse
import de.gesellix.util.QueryUtil
import groovy.transform.EqualsAndHashCode
import groovy.util.logging.Slf4j
@Slf4j
class ManageStackClient implements ManageStack {
private EngineClient client
private DockerResponseHandler responseHandler
private QueryUtil queryUtil
private ManageService manageService
private ManageTask manageTask
private ManageNode manageNode
private ManageNetwork manageNetwork
private ManageSecret manageSecret
private ManageConfig manageConfig
private ManageSystem manageSystem
private ManageAuthentication manageAuthentication
ManageStackClient(
EngineClient client,
DockerResponseHandler responseHandler,
ManageService manageService,
ManageTask manageTask,
ManageNode manageNode,
ManageNetwork manageNetwork,
ManageSecret manageSecret,
ManageConfig manageConfig,
ManageSystem manageSystem,
ManageAuthentication manageAuthentication) {
this.client = client
this.responseHandler = responseHandler
this.queryUtil = new QueryUtil()
this.manageService = manageService
this.manageTask = manageTask
this.manageNode = manageNode
this.manageNetwork = manageNetwork
this.manageSecret = manageSecret
this.manageConfig = manageConfig
this.manageSystem = manageSystem
this.manageAuthentication = manageAuthentication
}
@Override
Collection lsStacks() {
log.info "docker stack ls"
Map stacksByName = [:]
EngineResponse services = manageService.services([filters: [label: [(LabelNamespace): true]]])
services.content?.each { service ->
String stackName = service.Spec.Labels[(LabelNamespace)]
if (!stacksByName[(stackName)]) {
stacksByName[(stackName)] = new Stack(name: stackName, services: 0)
}
stacksByName[(stackName)].services++
}
return stacksByName.values()
}
Map toMap(object) {
return object?.properties?.findAll {
(it.key != 'class')
}?.collectEntries {
it.value == null || it.value instanceof Serializable ? [it.key, it.value] : [it.key, toMap(it.value)]
}
}
@Override
void stackDeploy(String namespace, DeployStackConfig config, DeployStackOptions options) {
log.info "docker stack deploy"
checkDaemonIsSwarmManager()
if (options.pruneServices) {
def serviceNames = config.services.keySet()
pruneServices(namespace, serviceNames)
}
createNetworks(namespace, config.networks)
Map changedSecrets = createSecrets(namespace, config.secrets)
Map changedConfigs = createConfigs(namespace, config.configs)
config.services.each { service ->
changedSecrets.each { name, res ->
service.value.taskTemplate?.containerSpec?.secrets?.find { it.SecretName == name }?.SecretID = res.content.ID
}
changedConfigs.each { name, res ->
service.value.taskTemplate?.containerSpec?.configs?.find { it.ConfigName == name }?.ConfigID = res.content.ID
}
}
createOrUpdateServices(namespace, config.services, options.sendRegistryAuth)
}
void createNetworks(String namespace, Map networks) {
def existingNetworks = manageNetwork.networks([
filters: [
label: [("${LabelNamespace}=${namespace}" as String): true]]])
def existingNetworkNames = []
existingNetworks.content.each {
existingNetworkNames << it.Name
}
networks.each { name, network ->
name = "${namespace}_${name}" as String
if (!existingNetworkNames.contains(name)) {
log.info("create network $name: $network")
if (!network.labels) {
network.labels = [:]
}
network.labels[(LabelNamespace)] = namespace
manageNetwork.createNetwork(name, toMap(network))
}
}
}
Map createSecrets(String namespace, Map secrets) {
return secrets.collectEntries { name, secret ->
List knownSecrets = manageSecret.secrets([filters: [name: [secret.name]]]).content
log.debug("known: $knownSecrets")
if (!secret.labels) {
secret.labels = [:]
}
secret.labels[(LabelNamespace)] = namespace
EngineResponse response
if (knownSecrets.empty) {
log.info("create secret ${secret.name}: $secret")
response = manageSecret.createSecret(secret.name, secret.data, secret.labels)
}
else {
if (knownSecrets.size() != 1) {
throw new IllegalStateException("ambiguous secret name '${secret.name}'")
}
def knownSecret = knownSecrets.first()
log.info("update secret ${secret.name}: $secret")
response = manageSecret.updateSecret(knownSecret.ID as String, knownSecret.Version.Index, toMap(secret))
}
return [(secret.name): response]
}
}
Map createConfigs(String namespace, Map configs) {
return configs.collectEntries { name, config ->
List knownConfigs = manageConfig.configs([filters: [name: [config.name]]]).content
log.debug("known: $knownConfigs")
if (!config.labels) {
config.labels = [:]
}
config.labels[(LabelNamespace)] = namespace
EngineResponse response
if (knownConfigs.empty) {
log.info("create config ${config.name}: $config")
response = manageConfig.createConfig(config.name, config.data, config.labels)
}
else {
if (knownConfigs.size() != 1) {
throw new IllegalStateException("ambiguous config name '${config.name}'")
}
def knownConfig = knownConfigs.first()
log.info("update config ${config.name}: $config")
response = manageConfig.updateConfig(knownConfig.ID as String, knownConfig.Version.Index, toMap(config))
}
return [(config.name): response]
}
}
void pruneServices(String namespace, Collection services) {
// Descope returns the name without the namespace prefix
def descope = { String name ->
return name.substring("${namespace}_".length())
}
def oldServices = stackServices(namespace)
def pruneServices = oldServices.content.findResults {
return services.contains(descope(it.Spec.Name as String)) ? null : it
}
pruneServices.each { service ->
manageService.rmService(service.ID)
}
}
void createOrUpdateServices(String namespace, Map services, boolean sendRegistryAuth) {
def existingServicesByName = [:]
def existingServices = stackServices(namespace)
existingServices.content.each { service ->
existingServicesByName[service.Spec.Name] = service
}
services.each { internalName, serviceSpec ->
def name = "${namespace}_${internalName}" as String
serviceSpec.name = serviceSpec.name ?: name
if (!serviceSpec.labels) {
serviceSpec.labels = [:]
}
serviceSpec.labels[(LabelNamespace)] = namespace
def encodedAuth = ""
if (sendRegistryAuth) {
// Retrieve encoded auth token from the image reference
String image = serviceSpec.taskTemplate.containerSpec.image
encodedAuth = manageAuthentication.retrieveEncodedAuthTokenForImage(image)
}
def service = existingServicesByName[name]
if (service) {
log.info("Updating service ${name} (id ${service.ID}): ${toMap(serviceSpec)}")
def updateOptions = [:]
if (sendRegistryAuth) {
updateOptions.EncodedRegistryAuth = encodedAuth
}
def response = manageService.updateService(
service.ID,
[version: service.Version.Index],
toMap(serviceSpec),
updateOptions)
response.content.Warnings.each { String warning ->
log.warn(warning)
}
}
else {
log.info("Creating service ${name}: ${serviceSpec}")
def createOptions = [:]
if (sendRegistryAuth) {
createOptions.EncodedRegistryAuth = encodedAuth
}
def response = manageService.createService(toMap(serviceSpec), createOptions)
}
}
}
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
// a swarm manager. This is necessary because we must create networks before we
// create services, but the API call for creating a network does not return a
// proper status code when it can't create a network in the "global" scope.
void checkDaemonIsSwarmManager() {
if (!manageSystem.info()?.content?.Swarm?.ControlAvailable) {
throw new IllegalStateException("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.")
}
}
@Override
EngineResponse stackPs(String namespace, Map filters = [:]) {
log.info "docker stack ps"
String namespaceFilter = "${LabelNamespace}=${namespace}"
def actualFilters = filters ?: [:]
if (actualFilters.label) {
actualFilters.label[(namespaceFilter)] = true
}
else {
actualFilters['label'] = [(namespaceFilter): true]
}
def tasks = manageTask.tasks([filters: actualFilters])
return tasks
}
@Override
void stackRm(String namespace) {
log.info "docker stack rm"
String namespaceFilter = "${LabelNamespace}=${namespace}"
def services = manageService.services([filters: [label: [(namespaceFilter): true]]])
def networks = manageNetwork.networks([filters: [label: [(namespaceFilter): true]]])
def secrets = manageSecret.secrets([filters: [label: [(namespaceFilter): true]]])
def configs = manageConfig.configs([filters: [label: [(namespaceFilter): true]]])
services.content.each { service ->
manageService.rmService(service.ID)
}
networks.content.each { network ->
manageNetwork.rmNetwork(network.Id)
}
secrets.content.each { secret ->
manageSecret.rmSecret(secret.ID as String)
}
configs.content.each { config ->
manageConfig.rmConfig(config.ID as String)
}
}
@Override
EngineResponse stackServices(String namespace, Map filters = [:]) {
log.info "docker stack services"
String namespaceFilter = "${LabelNamespace}=${namespace}"
def actualFilters = filters ?: [:]
if (actualFilters.label) {
actualFilters.label[(namespaceFilter)] = true
}
else {
actualFilters['label'] = [(namespaceFilter): true]
}
def services = manageService.services([filters: actualFilters])
// def infoByServiceId = getInfoByServiceId(services)
return services
}
def getInfoByServiceId(EngineResponse services) {
def nodes = manageNode.nodes()
List activeNodes = nodes.content.findResults { node ->
node.Status.State != 'down' ? node.ID : null
}
Map running = [:]
Map tasksNoShutdown = [:]
def serviceFilter = [service: [:]]
services.content.each { service ->
serviceFilter.service[(service.ID as String)] = true
}
def tasks = manageTask.tasks([filters: serviceFilter])
tasks.content.each { task ->
if (task.DesiredState != 'shutdown') {
if (!tasksNoShutdown[task.ServiceID as String]) {
tasksNoShutdown[task.ServiceID as String] = 0
}
tasksNoShutdown[task.ServiceID as String]++
}
if (activeNodes.contains(task.NodeID as String) && task.Status.State == 'running') {
if (!running[task.ServiceID as String]) {
running[task.ServiceID as String] = 0
}
running[task.ServiceID as String]++
}
}
def infoByServiceId = [:]
services.content.each { service ->
if (service.Spec.Mode.Replicated && service.Spec.Mode.Replicated.Replicas) {
infoByServiceId[service.ID] = new ServiceInfo(mode: 'replicated', replicas: "${running[service.ID as String] ?: 0}/${service.Spec.Mode.Replicated.Replicas}")
}
else if (service.Spec.Mode.Global) {
infoByServiceId[service.ID] = new ServiceInfo(mode: 'global', replicas: "${running[service.ID as String] ?: 0}}/${tasksNoShutdown[service.ID as String]}")
}
}
return infoByServiceId
}
@EqualsAndHashCode
static class Stack {
String name
int services
@Override
String toString() {
"$name: $services"
}
}
static class ServiceInfo {
String mode
String replicas
@Override
String toString() {
"$mode, $replicas"
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy