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