| package convert |
| |
| import ( |
| "fmt" |
| "os" |
| "sort" |
| "time" |
| |
| "github.com/docker/docker/api/types" |
| "github.com/docker/docker/api/types/container" |
| "github.com/docker/docker/api/types/swarm" |
| servicecli "github.com/docker/docker/cli/command/service" |
| composetypes "github.com/docker/docker/cli/compose/types" |
| "github.com/docker/docker/client" |
| "github.com/docker/docker/opts" |
| runconfigopts "github.com/docker/docker/runconfig/opts" |
| "github.com/docker/go-connections/nat" |
| ) |
| |
| // Services from compose-file types to engine API types |
| // TODO: fix secrets API so that SecretAPIClient is not required here |
| func Services( |
| namespace Namespace, |
| config *composetypes.Config, |
| client client.SecretAPIClient, |
| ) (map[string]swarm.ServiceSpec, error) { |
| result := make(map[string]swarm.ServiceSpec) |
| |
| services := config.Services |
| volumes := config.Volumes |
| networks := config.Networks |
| |
| for _, service := range services { |
| |
| secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets) |
| if err != nil { |
| return nil, err |
| } |
| serviceSpec, err := convertService(namespace, service, networks, volumes, secrets) |
| if err != nil { |
| return nil, err |
| } |
| result[service.Name] = serviceSpec |
| } |
| |
| return result, nil |
| } |
| |
| func convertService( |
| namespace Namespace, |
| service composetypes.ServiceConfig, |
| networkConfigs map[string]composetypes.NetworkConfig, |
| volumes map[string]composetypes.VolumeConfig, |
| secrets []*swarm.SecretReference, |
| ) (swarm.ServiceSpec, error) { |
| name := namespace.Scope(service.Name) |
| |
| endpoint, err := convertEndpointSpec(service.Ports) |
| if err != nil { |
| return swarm.ServiceSpec{}, err |
| } |
| |
| mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas) |
| if err != nil { |
| return swarm.ServiceSpec{}, err |
| } |
| |
| mounts, err := Volumes(service.Volumes, volumes, namespace) |
| if err != nil { |
| // TODO: better error message (include service name) |
| return swarm.ServiceSpec{}, err |
| } |
| |
| resources, err := convertResources(service.Deploy.Resources) |
| if err != nil { |
| return swarm.ServiceSpec{}, err |
| } |
| |
| restartPolicy, err := convertRestartPolicy( |
| service.Restart, service.Deploy.RestartPolicy) |
| if err != nil { |
| return swarm.ServiceSpec{}, err |
| } |
| |
| healthcheck, err := convertHealthcheck(service.HealthCheck) |
| if err != nil { |
| return swarm.ServiceSpec{}, err |
| } |
| |
| networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name) |
| if err != nil { |
| return swarm.ServiceSpec{}, err |
| } |
| |
| var logDriver *swarm.Driver |
| if service.Logging != nil { |
| logDriver = &swarm.Driver{ |
| Name: service.Logging.Driver, |
| Options: service.Logging.Options, |
| } |
| } |
| |
| serviceSpec := swarm.ServiceSpec{ |
| Annotations: swarm.Annotations{ |
| Name: name, |
| Labels: AddStackLabel(namespace, service.Deploy.Labels), |
| }, |
| TaskTemplate: swarm.TaskSpec{ |
| ContainerSpec: swarm.ContainerSpec{ |
| Image: service.Image, |
| Command: service.Entrypoint, |
| Args: service.Command, |
| Hostname: service.Hostname, |
| Hosts: sortStrings(convertExtraHosts(service.ExtraHosts)), |
| Healthcheck: healthcheck, |
| Env: sortStrings(convertEnvironment(service.Environment)), |
| Labels: AddStackLabel(namespace, service.Labels), |
| Dir: service.WorkingDir, |
| User: service.User, |
| Mounts: mounts, |
| StopGracePeriod: service.StopGracePeriod, |
| TTY: service.Tty, |
| OpenStdin: service.StdinOpen, |
| Secrets: secrets, |
| }, |
| LogDriver: logDriver, |
| Resources: resources, |
| RestartPolicy: restartPolicy, |
| Placement: &swarm.Placement{ |
| Constraints: service.Deploy.Placement.Constraints, |
| }, |
| }, |
| EndpointSpec: endpoint, |
| Mode: mode, |
| Networks: networks, |
| UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), |
| } |
| |
| return serviceSpec, nil |
| } |
| |
| func sortStrings(strs []string) []string { |
| sort.Strings(strs) |
| return strs |
| } |
| |
| type byNetworkTarget []swarm.NetworkAttachmentConfig |
| |
| func (a byNetworkTarget) Len() int { return len(a) } |
| func (a byNetworkTarget) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
| func (a byNetworkTarget) Less(i, j int) bool { return a[i].Target < a[j].Target } |
| |
| func convertServiceNetworks( |
| networks map[string]*composetypes.ServiceNetworkConfig, |
| networkConfigs networkMap, |
| namespace Namespace, |
| name string, |
| ) ([]swarm.NetworkAttachmentConfig, error) { |
| if len(networks) == 0 { |
| return []swarm.NetworkAttachmentConfig{ |
| { |
| Target: namespace.Scope("default"), |
| Aliases: []string{name}, |
| }, |
| }, nil |
| } |
| |
| nets := []swarm.NetworkAttachmentConfig{} |
| for networkName, network := range networks { |
| networkConfig, ok := networkConfigs[networkName] |
| if !ok { |
| return []swarm.NetworkAttachmentConfig{}, fmt.Errorf( |
| "service %q references network %q, which is not declared", name, networkName) |
| } |
| var aliases []string |
| if network != nil { |
| aliases = network.Aliases |
| } |
| target := namespace.Scope(networkName) |
| if networkConfig.External.External { |
| target = networkConfig.External.Name |
| } |
| nets = append(nets, swarm.NetworkAttachmentConfig{ |
| Target: target, |
| Aliases: append(aliases, name), |
| }) |
| } |
| |
| sort.Sort(byNetworkTarget(nets)) |
| |
| return nets, nil |
| } |
| |
| // TODO: fix secrets API so that SecretAPIClient is not required here |
| func convertServiceSecrets( |
| client client.SecretAPIClient, |
| namespace Namespace, |
| secrets []composetypes.ServiceSecretConfig, |
| secretSpecs map[string]composetypes.SecretConfig, |
| ) ([]*swarm.SecretReference, error) { |
| opts := []*types.SecretRequestOption{} |
| for _, secret := range secrets { |
| target := secret.Target |
| if target == "" { |
| target = secret.Source |
| } |
| |
| source := namespace.Scope(secret.Source) |
| secretSpec := secretSpecs[secret.Source] |
| if secretSpec.External.External { |
| source = secretSpec.External.Name |
| } |
| |
| uid := secret.UID |
| gid := secret.GID |
| if uid == "" { |
| uid = "0" |
| } |
| if gid == "" { |
| gid = "0" |
| } |
| |
| opts = append(opts, &types.SecretRequestOption{ |
| Source: source, |
| Target: target, |
| UID: uid, |
| GID: gid, |
| Mode: os.FileMode(secret.Mode), |
| }) |
| } |
| |
| return servicecli.ParseSecrets(client, opts) |
| } |
| |
| func convertExtraHosts(extraHosts map[string]string) []string { |
| hosts := []string{} |
| for host, ip := range extraHosts { |
| hosts = append(hosts, fmt.Sprintf("%s %s", ip, host)) |
| } |
| return hosts |
| } |
| |
| func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) { |
| if healthcheck == nil { |
| return nil, nil |
| } |
| var ( |
| err error |
| timeout, interval time.Duration |
| retries int |
| ) |
| if healthcheck.Disable { |
| if len(healthcheck.Test) != 0 { |
| return nil, fmt.Errorf("test and disable can't be set at the same time") |
| } |
| return &container.HealthConfig{ |
| Test: []string{"NONE"}, |
| }, nil |
| |
| } |
| if healthcheck.Timeout != "" { |
| timeout, err = time.ParseDuration(healthcheck.Timeout) |
| if err != nil { |
| return nil, err |
| } |
| } |
| if healthcheck.Interval != "" { |
| interval, err = time.ParseDuration(healthcheck.Interval) |
| if err != nil { |
| return nil, err |
| } |
| } |
| if healthcheck.Retries != nil { |
| retries = int(*healthcheck.Retries) |
| } |
| return &container.HealthConfig{ |
| Test: healthcheck.Test, |
| Timeout: timeout, |
| Interval: interval, |
| Retries: retries, |
| }, nil |
| } |
| |
| func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { |
| // TODO: log if restart is being ignored |
| if source == nil { |
| policy, err := runconfigopts.ParseRestartPolicy(restart) |
| if err != nil { |
| return nil, err |
| } |
| switch { |
| case policy.IsNone(): |
| return nil, nil |
| case policy.IsAlways(), policy.IsUnlessStopped(): |
| return &swarm.RestartPolicy{ |
| Condition: swarm.RestartPolicyConditionAny, |
| }, nil |
| case policy.IsOnFailure(): |
| attempts := uint64(policy.MaximumRetryCount) |
| return &swarm.RestartPolicy{ |
| Condition: swarm.RestartPolicyConditionOnFailure, |
| MaxAttempts: &attempts, |
| }, nil |
| default: |
| return nil, fmt.Errorf("unknown restart policy: %s", restart) |
| } |
| } |
| return &swarm.RestartPolicy{ |
| Condition: swarm.RestartPolicyCondition(source.Condition), |
| Delay: source.Delay, |
| MaxAttempts: source.MaxAttempts, |
| Window: source.Window, |
| }, nil |
| } |
| |
| func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig { |
| if source == nil { |
| return nil |
| } |
| parallel := uint64(1) |
| if source.Parallelism != nil { |
| parallel = *source.Parallelism |
| } |
| return &swarm.UpdateConfig{ |
| Parallelism: parallel, |
| Delay: source.Delay, |
| FailureAction: source.FailureAction, |
| Monitor: source.Monitor, |
| MaxFailureRatio: source.MaxFailureRatio, |
| } |
| } |
| |
| func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { |
| resources := &swarm.ResourceRequirements{} |
| var err error |
| if source.Limits != nil { |
| var cpus int64 |
| if source.Limits.NanoCPUs != "" { |
| cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs) |
| if err != nil { |
| return nil, err |
| } |
| } |
| resources.Limits = &swarm.Resources{ |
| NanoCPUs: cpus, |
| MemoryBytes: int64(source.Limits.MemoryBytes), |
| } |
| } |
| if source.Reservations != nil { |
| var cpus int64 |
| if source.Reservations.NanoCPUs != "" { |
| cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs) |
| if err != nil { |
| return nil, err |
| } |
| } |
| resources.Reservations = &swarm.Resources{ |
| NanoCPUs: cpus, |
| MemoryBytes: int64(source.Reservations.MemoryBytes), |
| } |
| } |
| return resources, nil |
| |
| } |
| |
| type byPublishedPort []swarm.PortConfig |
| |
| func (a byPublishedPort) Len() int { return len(a) } |
| func (a byPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
| func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort } |
| |
| func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { |
| portConfigs := []swarm.PortConfig{} |
| ports, portBindings, err := nat.ParsePortSpecs(source) |
| if err != nil { |
| return nil, err |
| } |
| |
| for port := range ports { |
| portConfigs = append( |
| portConfigs, |
| opts.ConvertPortToPortConfig(port, portBindings)...) |
| } |
| |
| // Sorting to make sure these are always in the same order |
| sort.Sort(byPublishedPort(portConfigs)) |
| |
| return &swarm.EndpointSpec{Ports: portConfigs}, nil |
| } |
| |
| func convertEnvironment(source map[string]string) []string { |
| var output []string |
| |
| for name, value := range source { |
| output = append(output, fmt.Sprintf("%s=%s", name, value)) |
| } |
| |
| return output |
| } |
| |
| func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) { |
| serviceMode := swarm.ServiceMode{} |
| |
| switch mode { |
| case "global": |
| if replicas != nil { |
| return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") |
| } |
| serviceMode.Global = &swarm.GlobalService{} |
| case "replicated", "": |
| serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} |
| default: |
| return serviceMode, fmt.Errorf("Unknown mode: %s", mode) |
| } |
| return serviceMode, nil |
| } |