github.com/kunnos/engine@v1.13.1/cli/compose/convert/service.go (about) 1 package convert 2 3 import ( 4 "fmt" 5 "os" 6 "sort" 7 "time" 8 9 "github.com/docker/docker/api/types" 10 "github.com/docker/docker/api/types/container" 11 "github.com/docker/docker/api/types/swarm" 12 servicecli "github.com/docker/docker/cli/command/service" 13 composetypes "github.com/docker/docker/cli/compose/types" 14 "github.com/docker/docker/client" 15 "github.com/docker/docker/opts" 16 runconfigopts "github.com/docker/docker/runconfig/opts" 17 "github.com/docker/go-connections/nat" 18 ) 19 20 // Services from compose-file types to engine API types 21 // TODO: fix secrets API so that SecretAPIClient is not required here 22 func Services( 23 namespace Namespace, 24 config *composetypes.Config, 25 client client.SecretAPIClient, 26 ) (map[string]swarm.ServiceSpec, error) { 27 result := make(map[string]swarm.ServiceSpec) 28 29 services := config.Services 30 volumes := config.Volumes 31 networks := config.Networks 32 33 for _, service := range services { 34 35 secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets) 36 if err != nil { 37 return nil, err 38 } 39 serviceSpec, err := convertService(namespace, service, networks, volumes, secrets) 40 if err != nil { 41 return nil, err 42 } 43 result[service.Name] = serviceSpec 44 } 45 46 return result, nil 47 } 48 49 func convertService( 50 namespace Namespace, 51 service composetypes.ServiceConfig, 52 networkConfigs map[string]composetypes.NetworkConfig, 53 volumes map[string]composetypes.VolumeConfig, 54 secrets []*swarm.SecretReference, 55 ) (swarm.ServiceSpec, error) { 56 name := namespace.Scope(service.Name) 57 58 endpoint, err := convertEndpointSpec(service.Ports) 59 if err != nil { 60 return swarm.ServiceSpec{}, err 61 } 62 63 mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas) 64 if err != nil { 65 return swarm.ServiceSpec{}, err 66 } 67 68 mounts, err := Volumes(service.Volumes, volumes, namespace) 69 if err != nil { 70 // TODO: better error message (include service name) 71 return swarm.ServiceSpec{}, err 72 } 73 74 resources, err := convertResources(service.Deploy.Resources) 75 if err != nil { 76 return swarm.ServiceSpec{}, err 77 } 78 79 restartPolicy, err := convertRestartPolicy( 80 service.Restart, service.Deploy.RestartPolicy) 81 if err != nil { 82 return swarm.ServiceSpec{}, err 83 } 84 85 healthcheck, err := convertHealthcheck(service.HealthCheck) 86 if err != nil { 87 return swarm.ServiceSpec{}, err 88 } 89 90 networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name) 91 if err != nil { 92 return swarm.ServiceSpec{}, err 93 } 94 95 var logDriver *swarm.Driver 96 if service.Logging != nil { 97 logDriver = &swarm.Driver{ 98 Name: service.Logging.Driver, 99 Options: service.Logging.Options, 100 } 101 } 102 103 serviceSpec := swarm.ServiceSpec{ 104 Annotations: swarm.Annotations{ 105 Name: name, 106 Labels: AddStackLabel(namespace, service.Deploy.Labels), 107 }, 108 TaskTemplate: swarm.TaskSpec{ 109 ContainerSpec: swarm.ContainerSpec{ 110 Image: service.Image, 111 Command: service.Entrypoint, 112 Args: service.Command, 113 Hostname: service.Hostname, 114 Hosts: sortStrings(convertExtraHosts(service.ExtraHosts)), 115 Healthcheck: healthcheck, 116 Env: sortStrings(convertEnvironment(service.Environment)), 117 Labels: AddStackLabel(namespace, service.Labels), 118 Dir: service.WorkingDir, 119 User: service.User, 120 Mounts: mounts, 121 StopGracePeriod: service.StopGracePeriod, 122 TTY: service.Tty, 123 OpenStdin: service.StdinOpen, 124 Secrets: secrets, 125 }, 126 LogDriver: logDriver, 127 Resources: resources, 128 RestartPolicy: restartPolicy, 129 Placement: &swarm.Placement{ 130 Constraints: service.Deploy.Placement.Constraints, 131 }, 132 }, 133 EndpointSpec: endpoint, 134 Mode: mode, 135 Networks: networks, 136 UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), 137 } 138 139 return serviceSpec, nil 140 } 141 142 func sortStrings(strs []string) []string { 143 sort.Strings(strs) 144 return strs 145 } 146 147 type byNetworkTarget []swarm.NetworkAttachmentConfig 148 149 func (a byNetworkTarget) Len() int { return len(a) } 150 func (a byNetworkTarget) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 151 func (a byNetworkTarget) Less(i, j int) bool { return a[i].Target < a[j].Target } 152 153 func convertServiceNetworks( 154 networks map[string]*composetypes.ServiceNetworkConfig, 155 networkConfigs networkMap, 156 namespace Namespace, 157 name string, 158 ) ([]swarm.NetworkAttachmentConfig, error) { 159 if len(networks) == 0 { 160 return []swarm.NetworkAttachmentConfig{ 161 { 162 Target: namespace.Scope("default"), 163 Aliases: []string{name}, 164 }, 165 }, nil 166 } 167 168 nets := []swarm.NetworkAttachmentConfig{} 169 for networkName, network := range networks { 170 networkConfig, ok := networkConfigs[networkName] 171 if !ok { 172 return []swarm.NetworkAttachmentConfig{}, fmt.Errorf( 173 "service %q references network %q, which is not declared", name, networkName) 174 } 175 var aliases []string 176 if network != nil { 177 aliases = network.Aliases 178 } 179 target := namespace.Scope(networkName) 180 if networkConfig.External.External { 181 target = networkConfig.External.Name 182 } 183 nets = append(nets, swarm.NetworkAttachmentConfig{ 184 Target: target, 185 Aliases: append(aliases, name), 186 }) 187 } 188 189 sort.Sort(byNetworkTarget(nets)) 190 191 return nets, nil 192 } 193 194 // TODO: fix secrets API so that SecretAPIClient is not required here 195 func convertServiceSecrets( 196 client client.SecretAPIClient, 197 namespace Namespace, 198 secrets []composetypes.ServiceSecretConfig, 199 secretSpecs map[string]composetypes.SecretConfig, 200 ) ([]*swarm.SecretReference, error) { 201 opts := []*types.SecretRequestOption{} 202 for _, secret := range secrets { 203 target := secret.Target 204 if target == "" { 205 target = secret.Source 206 } 207 208 source := namespace.Scope(secret.Source) 209 secretSpec := secretSpecs[secret.Source] 210 if secretSpec.External.External { 211 source = secretSpec.External.Name 212 } 213 214 uid := secret.UID 215 gid := secret.GID 216 if uid == "" { 217 uid = "0" 218 } 219 if gid == "" { 220 gid = "0" 221 } 222 223 opts = append(opts, &types.SecretRequestOption{ 224 Source: source, 225 Target: target, 226 UID: uid, 227 GID: gid, 228 Mode: os.FileMode(secret.Mode), 229 }) 230 } 231 232 return servicecli.ParseSecrets(client, opts) 233 } 234 235 func convertExtraHosts(extraHosts map[string]string) []string { 236 hosts := []string{} 237 for host, ip := range extraHosts { 238 hosts = append(hosts, fmt.Sprintf("%s %s", ip, host)) 239 } 240 return hosts 241 } 242 243 func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) { 244 if healthcheck == nil { 245 return nil, nil 246 } 247 var ( 248 err error 249 timeout, interval time.Duration 250 retries int 251 ) 252 if healthcheck.Disable { 253 if len(healthcheck.Test) != 0 { 254 return nil, fmt.Errorf("test and disable can't be set at the same time") 255 } 256 return &container.HealthConfig{ 257 Test: []string{"NONE"}, 258 }, nil 259 260 } 261 if healthcheck.Timeout != "" { 262 timeout, err = time.ParseDuration(healthcheck.Timeout) 263 if err != nil { 264 return nil, err 265 } 266 } 267 if healthcheck.Interval != "" { 268 interval, err = time.ParseDuration(healthcheck.Interval) 269 if err != nil { 270 return nil, err 271 } 272 } 273 if healthcheck.Retries != nil { 274 retries = int(*healthcheck.Retries) 275 } 276 return &container.HealthConfig{ 277 Test: healthcheck.Test, 278 Timeout: timeout, 279 Interval: interval, 280 Retries: retries, 281 }, nil 282 } 283 284 func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { 285 // TODO: log if restart is being ignored 286 if source == nil { 287 policy, err := runconfigopts.ParseRestartPolicy(restart) 288 if err != nil { 289 return nil, err 290 } 291 switch { 292 case policy.IsNone(): 293 return nil, nil 294 case policy.IsAlways(), policy.IsUnlessStopped(): 295 return &swarm.RestartPolicy{ 296 Condition: swarm.RestartPolicyConditionAny, 297 }, nil 298 case policy.IsOnFailure(): 299 attempts := uint64(policy.MaximumRetryCount) 300 return &swarm.RestartPolicy{ 301 Condition: swarm.RestartPolicyConditionOnFailure, 302 MaxAttempts: &attempts, 303 }, nil 304 default: 305 return nil, fmt.Errorf("unknown restart policy: %s", restart) 306 } 307 } 308 return &swarm.RestartPolicy{ 309 Condition: swarm.RestartPolicyCondition(source.Condition), 310 Delay: source.Delay, 311 MaxAttempts: source.MaxAttempts, 312 Window: source.Window, 313 }, nil 314 } 315 316 func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig { 317 if source == nil { 318 return nil 319 } 320 parallel := uint64(1) 321 if source.Parallelism != nil { 322 parallel = *source.Parallelism 323 } 324 return &swarm.UpdateConfig{ 325 Parallelism: parallel, 326 Delay: source.Delay, 327 FailureAction: source.FailureAction, 328 Monitor: source.Monitor, 329 MaxFailureRatio: source.MaxFailureRatio, 330 } 331 } 332 333 func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { 334 resources := &swarm.ResourceRequirements{} 335 var err error 336 if source.Limits != nil { 337 var cpus int64 338 if source.Limits.NanoCPUs != "" { 339 cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs) 340 if err != nil { 341 return nil, err 342 } 343 } 344 resources.Limits = &swarm.Resources{ 345 NanoCPUs: cpus, 346 MemoryBytes: int64(source.Limits.MemoryBytes), 347 } 348 } 349 if source.Reservations != nil { 350 var cpus int64 351 if source.Reservations.NanoCPUs != "" { 352 cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs) 353 if err != nil { 354 return nil, err 355 } 356 } 357 resources.Reservations = &swarm.Resources{ 358 NanoCPUs: cpus, 359 MemoryBytes: int64(source.Reservations.MemoryBytes), 360 } 361 } 362 return resources, nil 363 364 } 365 366 type byPublishedPort []swarm.PortConfig 367 368 func (a byPublishedPort) Len() int { return len(a) } 369 func (a byPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 370 func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort } 371 372 func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { 373 portConfigs := []swarm.PortConfig{} 374 ports, portBindings, err := nat.ParsePortSpecs(source) 375 if err != nil { 376 return nil, err 377 } 378 379 for port := range ports { 380 portConfigs = append( 381 portConfigs, 382 opts.ConvertPortToPortConfig(port, portBindings)...) 383 } 384 385 // Sorting to make sure these are always in the same order 386 sort.Sort(byPublishedPort(portConfigs)) 387 388 return &swarm.EndpointSpec{Ports: portConfigs}, nil 389 } 390 391 func convertEnvironment(source map[string]string) []string { 392 var output []string 393 394 for name, value := range source { 395 output = append(output, fmt.Sprintf("%s=%s", name, value)) 396 } 397 398 return output 399 } 400 401 func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) { 402 serviceMode := swarm.ServiceMode{} 403 404 switch mode { 405 case "global": 406 if replicas != nil { 407 return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") 408 } 409 serviceMode.Global = &swarm.GlobalService{} 410 case "replicated", "": 411 serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} 412 default: 413 return serviceMode, fmt.Errorf("Unknown mode: %s", mode) 414 } 415 return serviceMode, nil 416 }