package docker import ( "bytes" "context" "errors" "io" "sort" "strconv" "strings" "time" "github.com/cuigh/auxo/ext/texts" "github.com/cuigh/swirl/misc" "github.com/cuigh/swirl/model" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" ) // ServiceList return service list. func ServiceList(name string, pageIndex, pageSize int) (infos []*model.ServiceListInfo, totalCount int, err error) { // nolint: gocyclo err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) { var ( services []swarm.Service nodes []swarm.Node activeNodes map[string]struct{} tasks []swarm.Task ) // acquire services services, err = cli.ServiceList(ctx, types.ServiceListOptions{}) if err != nil { return } if name != "" { j := 0 name = strings.ToLower(name) for i, l := 0, len(services); i < l; i++ { if strings.Contains(strings.ToLower(services[i].Spec.Name), name) { services[j] = services[i] j++ } } services = services[:j] } sort.Slice(services, func(i, j int) bool { return services[i].Spec.Name < services[j].Spec.Name }) totalCount = len(services) start, end := misc.Page(totalCount, pageIndex, pageSize) services = services[start:end] // acquire all swarm nodes nodes, err = cli.NodeList(ctx, types.NodeListOptions{}) if err != nil { return } activeNodes = make(map[string]struct{}) for _, n := range nodes { if n.Status.State != swarm.NodeStateDown { activeNodes[n.ID] = struct{}{} } } // acquire all related tasks taskOpts := types.TaskListOptions{ Filters: filters.NewArgs(), } for _, service := range services { taskOpts.Filters.Add("service", service.ID) } tasks, err = cli.TaskList(ctx, taskOpts) if err != nil { return } // count active tasks running, tasksNoShutdown := map[string]uint64{}, map[string]uint64{} for _, task := range tasks { if task.DesiredState != swarm.TaskStateShutdown { tasksNoShutdown[task.ServiceID]++ } if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning { running[task.ServiceID]++ } } infos = make([]*model.ServiceListInfo, len(services)) for i, service := range services { infos[i] = model.NewServiceListInfo(service, running[service.ID]) if service.Spec.Mode.Global != nil { infos[i].Replicas = tasksNoShutdown[service.ID] } } return }) return } // ServiceSearch search services with args. func ServiceSearch(args filters.Args) (services []swarm.Service, err error) { // nolint: gocyclo err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) { opts := types.ServiceListOptions{Filters: args} services, err = cli.ServiceList(ctx, opts) return }) return } // ServiceCount return number of services. func ServiceCount() (count int, err error) { err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) { var services []swarm.Service if services, err = cli.ServiceList(ctx, types.ServiceListOptions{}); err == nil { count = len(services) } return }) return } // ServiceInspect return service raw information. func ServiceInspect(name string) (service swarm.Service, raw []byte, err error) { err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) { service, raw, err = cli.ServiceInspectWithRaw(ctx, name, types.ServiceInspectOptions{}) return }) return } // ServiceUpdate update a service. func ServiceUpdate(info *model.ServiceInfo) error { // nolint: gocyclo return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) { service, _, err := cli.ServiceInspectWithRaw(ctx, info.Name, types.ServiceInspectOptions{}) if err != nil { return err } spec := service.Spec // Annotations spec.Annotations.Labels = info.ServiceLabels.ToMap() // ContainerSpec spec.TaskTemplate.ContainerSpec.Image = info.Image spec.TaskTemplate.ContainerSpec.Dir = info.Dir spec.TaskTemplate.ContainerSpec.User = info.User spec.TaskTemplate.ContainerSpec.Labels = info.ContainerLabels.ToMap() spec.TaskTemplate.ContainerSpec.Command = nil spec.TaskTemplate.ContainerSpec.Args = nil spec.TaskTemplate.ContainerSpec.Env = nil if info.Command != "" { spec.TaskTemplate.ContainerSpec.Command = strings.Split(info.Command, " ") } if info.Args != "" { spec.TaskTemplate.ContainerSpec.Args = strings.Split(info.Args, " ") } if envs := info.Environments.ToMap(); len(envs) > 0 { for n, v := range envs { spec.TaskTemplate.ContainerSpec.Env = append(spec.TaskTemplate.ContainerSpec.Env, n+"="+v) } } // Mode if info.Mode == "replicated" { if spec.Mode.Replicated == nil { spec.Mode.Global = nil spec.Mode.Replicated = &swarm.ReplicatedService{Replicas: &info.Replicas} } else { spec.Mode.Replicated.Replicas = &info.Replicas } } else if info.Mode == "global" && spec.Mode.Global == nil { spec.Mode.Replicated = nil spec.Mode.Global = &swarm.GlobalService{} } // Network // todo: only process updated networks spec.TaskTemplate.Networks = nil for _, n := range info.Networks { spec.TaskTemplate.Networks = append(spec.TaskTemplate.Networks, swarm.NetworkAttachmentConfig{Target: n}) } // Endpoint spec.EndpointSpec = &swarm.EndpointSpec{Mode: swarm.ResolutionMode(info.Endpoint.Mode)} for _, p := range info.Endpoint.Ports { port := swarm.PortConfig{ Protocol: p.Protocol, TargetPort: p.TargetPort, PublishedPort: p.PublishedPort, PublishMode: p.PublishMode, } spec.EndpointSpec.Ports = append(spec.EndpointSpec.Ports, port) } spec.TaskTemplate.ContainerSpec.Secrets = nil for _, s := range info.Secrets { spec.TaskTemplate.ContainerSpec.Secrets = append(spec.TaskTemplate.ContainerSpec.Secrets, s.ToSecret()) } spec.TaskTemplate.ContainerSpec.Configs = nil for _, c := range info.Configs { spec.TaskTemplate.ContainerSpec.Configs = append(spec.TaskTemplate.ContainerSpec.Configs, c.ToConfig()) } // Mounts // todo: fix > original options are not reserved. spec.TaskTemplate.ContainerSpec.Mounts = nil for _, m := range info.Mounts { if m.Target != "" { mnt := mount.Mount{ Type: mount.Type(m.Type), Source: m.Source, Target: m.Target, ReadOnly: m.ReadOnly, } if m.Propagation != "" { mnt.BindOptions = &mount.BindOptions{Propagation: m.Propagation} } spec.TaskTemplate.ContainerSpec.Mounts = append(spec.TaskTemplate.ContainerSpec.Mounts, mnt) } } // Placement if spec.TaskTemplate.Placement == nil { spec.TaskTemplate.Placement = &swarm.Placement{} } spec.TaskTemplate.Placement.Constraints = nil for _, c := range info.Placement.Constraints { if cons := c.ToConstraint(); cons != "" { spec.TaskTemplate.Placement.Constraints = append(spec.TaskTemplate.Placement.Constraints, cons) } } spec.TaskTemplate.Placement.Preferences = nil for _, p := range info.Placement.Preferences { if p.Spread != "" { pref := swarm.PlacementPreference{ Spread: &swarm.SpreadOver{SpreadDescriptor: p.Spread}, } spec.TaskTemplate.Placement.Preferences = append(spec.TaskTemplate.Placement.Preferences, pref) } } // update policy spec.UpdateConfig = &swarm.UpdateConfig{ Parallelism: info.UpdatePolicy.Parallelism, FailureAction: info.UpdatePolicy.FailureAction, Order: info.UpdatePolicy.Order, } if info.UpdatePolicy.Delay != "" { spec.UpdateConfig.Delay, err = time.ParseDuration(info.UpdatePolicy.Delay) if err != nil { return err } } // rollback policy spec.RollbackConfig = &swarm.UpdateConfig{ Parallelism: info.RollbackPolicy.Parallelism, FailureAction: info.RollbackPolicy.FailureAction, Order: info.RollbackPolicy.Order, } if info.RollbackPolicy.Delay != "" { spec.RollbackConfig.Delay, err = time.ParseDuration(info.RollbackPolicy.Delay) if err != nil { return err } } // restart policy var d time.Duration spec.TaskTemplate.RestartPolicy = &swarm.RestartPolicy{ Condition: info.RestartPolicy.Condition, MaxAttempts: &info.RestartPolicy.MaxAttempts, } if info.RestartPolicy.Delay != "" { d, err = time.ParseDuration(info.RestartPolicy.Delay) if err != nil { return err } spec.TaskTemplate.RestartPolicy.Delay = &d } if info.RestartPolicy.Window != "" { d, err = time.ParseDuration(info.RestartPolicy.Window) if err != nil { return err } spec.TaskTemplate.RestartPolicy.Window = &d } // resources if info.Resource.Limit.IsSet() || info.Resource.Reserve.IsSet() { spec.TaskTemplate.Resources = &swarm.ResourceRequirements{} if info.Resource.Limit.IsSet() { spec.TaskTemplate.Resources.Limits, err = info.Resource.Limit.ToResources() if err != nil { return err } } if info.Resource.Limit.IsSet() { spec.TaskTemplate.Resources.Reservations, err = info.Resource.Reserve.ToResources() if err != nil { return err } } } // log driver if info.LogDriver.Name == "" { spec.TaskTemplate.LogDriver = nil } else { spec.TaskTemplate.LogDriver = &swarm.Driver{ Name: info.LogDriver.Name, Options: info.LogDriver.Options.ToMap(), } } // Host & DNS spec.TaskTemplate.ContainerSpec.Hostname = info.Hostname if info.Hosts != "" { spec.TaskTemplate.ContainerSpec.Hosts = strings.Split(info.Hosts, "\n") } spec.TaskTemplate.ContainerSpec.DNSConfig = info.GetDNSConfig() options := types.ServiceUpdateOptions{ RegistryAuthFrom: types.RegistryAuthFromSpec, QueryRegistry: false, } resp, err := cli.ServiceUpdate(context.Background(), info.Name, version(info.Version), spec, options) if err == nil && len(resp.Warnings) > 0 { mgr.Logger().Warnf("service %s was updated but got warnings: %v", info.Name, resp.Warnings) } return err }) } // ServiceScale adjust replicas of a service. func ServiceScale(name string, version, count uint64) error { return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) { service, _, err := cli.ServiceInspectWithRaw(ctx, name, types.ServiceInspectOptions{}) if err != nil { return err } spec := service.Spec if spec.Mode.Replicated == nil { return errors.New("scale can only be used with replicated mode") } spec.Mode.Replicated.Replicas = &count ver := service.Version if version > 0 { ver = swarm.Version{Index: version} } resp, err := cli.ServiceUpdate(context.Background(), name, ver, spec, types.ServiceUpdateOptions{}) if err == nil && len(resp.Warnings) > 0 { mgr.Logger().Warnf("service %s was scaled but got warnings: %v", name, resp.Warnings) } return err }) } // ServiceCreate create a service. func ServiceCreate(info *model.ServiceInfo) error { // nolint: gocyclo return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) { service := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: info.Name, Labels: info.ServiceLabels.ToMap(), }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: &swarm.ContainerSpec{ Image: info.Image, Dir: info.Dir, User: info.User, Labels: info.ContainerLabels.ToMap(), }, }, EndpointSpec: &swarm.EndpointSpec{Mode: swarm.ResolutionModeVIP}, } if info.Command != "" { service.TaskTemplate.ContainerSpec.Command = strings.Split(info.Command, " ") } if info.Args != "" { service.TaskTemplate.ContainerSpec.Args = strings.Split(info.Args, " ") } if info.Mode == "replicated" { service.Mode.Replicated = &swarm.ReplicatedService{Replicas: &info.Replicas} } else if info.Mode == "global" { service.Mode.Global = &swarm.GlobalService{} } if envs := info.Environments.ToMap(); len(envs) > 0 { for n, v := range envs { service.TaskTemplate.ContainerSpec.Env = append(service.TaskTemplate.ContainerSpec.Env, n+"="+v) } } for _, n := range info.Networks { service.TaskTemplate.Networks = append(service.TaskTemplate.Networks, swarm.NetworkAttachmentConfig{Target: n}) } if info.Endpoint.Mode != "" && len(info.Endpoint.Ports) > 0 { service.EndpointSpec = &swarm.EndpointSpec{Mode: swarm.ResolutionMode(info.Endpoint.Mode)} for _, p := range info.Endpoint.Ports { port := swarm.PortConfig{ Protocol: p.Protocol, TargetPort: p.TargetPort, PublishedPort: p.PublishedPort, PublishMode: p.PublishMode, } service.EndpointSpec.Ports = append(service.EndpointSpec.Ports, port) } } for _, s := range info.Secrets { service.TaskTemplate.ContainerSpec.Secrets = append(service.TaskTemplate.ContainerSpec.Secrets, s.ToSecret()) } for _, c := range info.Configs { service.TaskTemplate.ContainerSpec.Configs = append(service.TaskTemplate.ContainerSpec.Configs, c.ToConfig()) } for _, m := range info.Mounts { if m.Target != "" { mnt := mount.Mount{ Type: mount.Type(m.Type), Source: m.Source, Target: m.Target, ReadOnly: m.ReadOnly, } if m.Propagation != "" { mnt.BindOptions = &mount.BindOptions{Propagation: m.Propagation} } service.TaskTemplate.ContainerSpec.Mounts = append(service.TaskTemplate.ContainerSpec.Mounts, mnt) } } service.TaskTemplate.Placement = &swarm.Placement{} for _, c := range info.Placement.Constraints { if cons := c.ToConstraint(); cons != "" { service.TaskTemplate.Placement.Constraints = append(service.TaskTemplate.Placement.Constraints, cons) } } for _, p := range info.Placement.Preferences { if p.Spread != "" { pref := swarm.PlacementPreference{ Spread: &swarm.SpreadOver{SpreadDescriptor: p.Spread}, } service.TaskTemplate.Placement.Preferences = append(service.TaskTemplate.Placement.Preferences, pref) } } // update policy service.UpdateConfig = &swarm.UpdateConfig{ Parallelism: info.UpdatePolicy.Parallelism, FailureAction: info.UpdatePolicy.FailureAction, Order: info.UpdatePolicy.Order, } if info.UpdatePolicy.Delay != "" { service.UpdateConfig.Delay, err = time.ParseDuration(info.UpdatePolicy.Delay) if err != nil { return err } } // rollback policy service.RollbackConfig = &swarm.UpdateConfig{ Parallelism: info.RollbackPolicy.Parallelism, FailureAction: info.RollbackPolicy.FailureAction, Order: info.RollbackPolicy.Order, } if info.RollbackPolicy.Delay != "" { service.RollbackConfig.Delay, err = time.ParseDuration(info.RollbackPolicy.Delay) if err != nil { return err } } // restart policy var d time.Duration service.TaskTemplate.RestartPolicy = &swarm.RestartPolicy{ Condition: info.RestartPolicy.Condition, MaxAttempts: &info.RestartPolicy.MaxAttempts, } if info.RestartPolicy.Delay != "" { d, err = time.ParseDuration(info.RestartPolicy.Delay) if err != nil { return err } service.TaskTemplate.RestartPolicy.Delay = &d } if info.RestartPolicy.Window != "" { d, err = time.ParseDuration(info.RestartPolicy.Window) if err != nil { return err } service.TaskTemplate.RestartPolicy.Window = &d } // resources if info.Resource.Limit.IsSet() || info.Resource.Reserve.IsSet() { service.TaskTemplate.Resources = &swarm.ResourceRequirements{} if info.Resource.Limit.IsSet() { service.TaskTemplate.Resources.Limits, err = info.Resource.Limit.ToResources() if err != nil { return err } } if info.Resource.Limit.IsSet() { service.TaskTemplate.Resources.Reservations, err = info.Resource.Reserve.ToResources() if err != nil { return err } } } // log driver if info.LogDriver.Name != "" { service.TaskTemplate.LogDriver = &swarm.Driver{ Name: info.LogDriver.Name, Options: info.LogDriver.Options.ToMap(), } } // Host & DNS service.TaskTemplate.ContainerSpec.Hostname = info.Hostname if info.Hosts != "" { service.TaskTemplate.ContainerSpec.Hosts = strings.Split(info.Hosts, "\n") } service.TaskTemplate.ContainerSpec.DNSConfig = info.GetDNSConfig() opts := types.ServiceCreateOptions{EncodedRegistryAuth: info.RegistryAuth} var resp types.ServiceCreateResponse resp, err = cli.ServiceCreate(ctx, service, opts) if err == nil && len(resp.Warnings) > 0 { mgr.Logger().Warnf("service %s was created but got warnings: %v", info.Name, resp.Warnings) } return }) } // ServiceRemove remove a service. func ServiceRemove(name string) error { return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) { return cli.ServiceRemove(ctx, name) }) } // ServiceLogs returns the logs generated by a service. func ServiceLogs(name string, line int, timestamps bool) (stdout, stderr *bytes.Buffer, err error) { var ( ctx context.Context cli *client.Client rc io.ReadCloser ) ctx, cli, err = mgr.Client() if err != nil { return } opts := types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, Tail: strconv.Itoa(line), Timestamps: timestamps, //Since: (time.Hour * 24).String() } if rc, err = cli.ServiceLogs(ctx, name, opts); err == nil { defer rc.Close() stdout = &bytes.Buffer{} stderr = &bytes.Buffer{} _, err = stdcopy.StdCopy(stdout, stderr, rc) } return } // ServiceCommand returns the docker command line to create this service. func ServiceCommand(name string) (cmd string, err error) { // nolint: gocyclo // todo: support auto wrapping var ( ctx context.Context cli *client.Client ) ctx, cli, err = mgr.Client() if err != nil { return } service, _, err := cli.ServiceInspectWithRaw(ctx, name, types.ServiceInspectOptions{}) if err != nil { return } si := model.NewServiceInfo(service) b := texts.Builder{} b.WriteString("docker service create --name ", si.Name) if si.Mode == "global" { b.WriteString(" --mode global") } else if si.Replicas > 1 { b.WriteFormat(" --replicas %d", si.Replicas) } for _, n := range service.Spec.TaskTemplate.Networks { var network types.NetworkResource network, err = cli.NetworkInspect(ctx, n.Target, types.NetworkInspectOptions{}) b.WriteString(" --network ", network.Name) } if len(si.Endpoint.Ports) > 0 { if si.Endpoint.Mode == swarm.ResolutionModeDNSRR { b.WriteFormat(" --endpoint-mode %s", si.Endpoint.Mode) } for _, p := range si.Endpoint.Ports { b.WriteFormat(" --publish mode=%s,target=%d,published=%d,protocol=%s", p.PublishMode, p.TargetPort, p.PublishedPort, p.Protocol) } } for _, c := range si.Placement.Constraints { b.WriteFormat(" --constraint '%s'", c.ToConstraint()) } for _, p := range si.Placement.Preferences { b.WriteFormat(" --placement-pref '%s'", p.Spread) } for _, e := range si.Environments { b.WriteFormat(" --env '%s=%s'", e.Name, e.Value) } for _, l := range si.ServiceLabels { b.WriteFormat(" --label '%s=%s'", l.Name, l.Value) } for _, l := range si.ContainerLabels { b.WriteFormat(" --container-label '%s=%s'", l.Name, l.Value) } for _, mnt := range si.Mounts { // todo: add mnt.Propagation b.WriteFormat(" --mount type=%s,source=%s,destination=%s", mnt.Type, mnt.Source, mnt.Target) if mnt.ReadOnly { b.WriteString(",ro=1") } } // todo: uid/gid for _, c := range si.Configs { b.WriteFormat(" --config source=%s,target=%s,mode=0%d", c.Name, c.FileName, c.Mode) } for _, c := range si.Secrets { b.WriteFormat(" --secret source=%s,target=%s,mode=0%d", c.Name, c.FileName, c.Mode) } if si.Resource.Limit.IsSet() { if si.Resource.Limit.CPU > 0 { b.WriteFormat(" --limit-cpu %.2f", si.Resource.Limit.CPU) } if si.Resource.Limit.Memory != "" { b.WriteString(" --limit-memory ", si.Resource.Limit.Memory) } } if si.Resource.Reserve.IsSet() { if si.Resource.Reserve.CPU > 0 { b.WriteFormat(" --reserve-cpu %.2f", si.Resource.Reserve.CPU) } if si.Resource.Reserve.Memory != "" { b.WriteString(" --reserve-memory ", si.Resource.Reserve.Memory) } } if si.LogDriver.Name != "" { b.WriteString(" --log-driver ", si.LogDriver.Name) for _, opt := range si.LogDriver.Options { b.WriteFormat(" --log-opt '%s=%s'", opt.Name, opt.Value) } } // UpdatePolicy if si.UpdatePolicy.Parallelism > 0 { b.WriteFormat(" --update-parallelism %d", si.UpdatePolicy.Parallelism) } if si.UpdatePolicy.Delay != "" { b.WriteString(" --update-delay ", si.UpdatePolicy.Delay) } if si.UpdatePolicy.FailureAction != "" { b.WriteString(" --update-failure-action ", si.UpdatePolicy.FailureAction) } if si.UpdatePolicy.Order != "" { b.WriteString(" --update-order ", si.UpdatePolicy.Order) } // RollbackPolicy if si.RollbackPolicy.Parallelism > 0 { b.WriteFormat(" --rollback-parallelism %d", si.RollbackPolicy.Parallelism) } if si.RollbackPolicy.Delay != "" { b.WriteString(" --rollback-delay ", si.RollbackPolicy.Delay) } if si.RollbackPolicy.FailureAction != "" { b.WriteString(" --rollback-failure-action ", si.RollbackPolicy.FailureAction) } if si.RollbackPolicy.Order != "" { b.WriteString(" --rollback-order ", si.RollbackPolicy.Order) } // RestartPolicy if si.RestartPolicy.Condition != "" { b.WriteFormat(" --restart-condition %s", si.RestartPolicy.Condition) } if si.RestartPolicy.MaxAttempts > 0 { b.WriteFormat(" --restart-max-attempts %d", si.RestartPolicy.MaxAttempts) } if si.RestartPolicy.Delay != "" { b.WriteString(" --restart-delay ", si.RestartPolicy.Delay) } if si.RestartPolicy.Window != "" { b.WriteString(" --restart-window ", si.RestartPolicy.Window) } //b.WriteString(" --with-registry-auth") if si.Dir != "" { b.WriteString(" --workdir ", si.Dir) } if si.User != "" { b.WriteString(" --user ", si.User) } b.WriteString(" ", si.Image) if si.Command != "" { b.WriteString(" ", si.Command) } if si.Args != "" { b.WriteString(" ", si.Args) } cmd = b.String() return } // ServiceRollback rollbacks a service. func ServiceRollback(name string) error { return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) { service, _, err := cli.ServiceInspectWithRaw(ctx, name, types.ServiceInspectOptions{}) if err != nil { return err } options := types.ServiceUpdateOptions{ Rollback: "previous", } resp, err := cli.ServiceUpdate(context.Background(), name, service.Version, service.Spec, options) if err == nil && len(resp.Warnings) > 0 { mgr.Logger().Warnf("service %s was rollbacked but got warnings: %v", name, resp.Warnings) } return err }) } // ServiceRestart force to refresh a service. func ServiceRestart(name string) error { return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) { service, _, err := cli.ServiceInspectWithRaw(ctx, name, types.ServiceInspectOptions{}) if err != nil { return err } service.Spec.TaskTemplate.ForceUpdate++ resp, err := cli.ServiceUpdate(context.Background(), name, service.Version, service.Spec, types.ServiceUpdateOptions{}) if err == nil && len(resp.Warnings) > 0 { mgr.Logger().Warnf("service %s was restarted but got warnings: %v", name, resp.Warnings) } return err }) }