mirror of
https://github.com/sablierapp/sablier.git
synced 2025-12-21 13:23:03 +01:00
207 lines
5.2 KiB
Go
207 lines
5.2 KiB
Go
package dockerswarm
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/sablierapp/sablier/app/discovery"
|
|
"github.com/sablierapp/sablier/pkg/provider"
|
|
"io"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"github.com/docker/docker/client"
|
|
"github.com/sablierapp/sablier/app/instance"
|
|
)
|
|
|
|
// Interface guard
|
|
var _ provider.Provider = (*DockerSwarmProvider)(nil)
|
|
|
|
type DockerSwarmProvider struct {
|
|
Client client.APIClient
|
|
desiredReplicas int32
|
|
|
|
l *slog.Logger
|
|
}
|
|
|
|
func NewDockerSwarmProvider(ctx context.Context, logger *slog.Logger) (*DockerSwarmProvider, error) {
|
|
logger = logger.With(slog.String("provider", "swarm"))
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot create docker client: %w", err)
|
|
}
|
|
|
|
serverVersion, err := cli.ServerVersion(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot connect to docker host: %w", err)
|
|
}
|
|
|
|
logger.InfoContext(ctx, "connection established with docker swarm",
|
|
slog.String("version", serverVersion.Version),
|
|
slog.String("api_version", serverVersion.APIVersion),
|
|
)
|
|
|
|
return &DockerSwarmProvider{
|
|
Client: cli,
|
|
desiredReplicas: 1,
|
|
l: logger,
|
|
}, nil
|
|
|
|
}
|
|
|
|
func (p *DockerSwarmProvider) Start(ctx context.Context, name string) error {
|
|
return p.scale(ctx, name, uint64(p.desiredReplicas))
|
|
}
|
|
|
|
func (p *DockerSwarmProvider) Stop(ctx context.Context, name string) error {
|
|
return p.scale(ctx, name, 0)
|
|
}
|
|
|
|
func (p *DockerSwarmProvider) scale(ctx context.Context, name string, replicas uint64) error {
|
|
service, err := p.getServiceByName(name, ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
foundName := p.getInstanceName(name, *service)
|
|
if service.Spec.Mode.Replicated == nil {
|
|
return errors.New("swarm service is not in \"replicated\" mode")
|
|
}
|
|
|
|
service.Spec.Mode.Replicated.Replicas = &replicas
|
|
|
|
response, err := p.Client.ServiceUpdate(ctx, service.ID, service.Meta.Version, service.Spec, types.ServiceUpdateOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(response.Warnings) > 0 {
|
|
return fmt.Errorf("warning received updating swarm service [%s]: %s", foundName, strings.Join(response.Warnings, ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *DockerSwarmProvider) GetGroups(ctx context.Context) (map[string][]string, error) {
|
|
f := filters.NewArgs()
|
|
f.Add("label", fmt.Sprintf("%s=true", discovery.LabelEnable))
|
|
|
|
services, err := p.Client.ServiceList(ctx, types.ServiceListOptions{
|
|
Filters: f,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
groups := make(map[string][]string)
|
|
for _, service := range services {
|
|
groupName := service.Spec.Labels[discovery.LabelGroup]
|
|
if len(groupName) == 0 {
|
|
groupName = discovery.LabelGroupDefaultValue
|
|
}
|
|
|
|
group := groups[groupName]
|
|
group = append(group, service.Spec.Name)
|
|
groups[groupName] = group
|
|
}
|
|
|
|
return groups, nil
|
|
}
|
|
|
|
func (p *DockerSwarmProvider) GetState(ctx context.Context, name string) (instance.State, error) {
|
|
|
|
service, err := p.getServiceByName(name, ctx)
|
|
if err != nil {
|
|
return instance.State{}, err
|
|
}
|
|
|
|
foundName := p.getInstanceName(name, *service)
|
|
|
|
if service.Spec.Mode.Replicated == nil {
|
|
return instance.State{}, errors.New("swarm service is not in \"replicated\" mode")
|
|
}
|
|
|
|
if service.ServiceStatus.DesiredTasks != service.ServiceStatus.RunningTasks || service.ServiceStatus.DesiredTasks == 0 {
|
|
return instance.NotReadyInstanceState(foundName, 0, p.desiredReplicas), nil
|
|
}
|
|
|
|
return instance.ReadyInstanceState(foundName, p.desiredReplicas), nil
|
|
}
|
|
|
|
func (p *DockerSwarmProvider) getServiceByName(name string, ctx context.Context) (*swarm.Service, error) {
|
|
opts := types.ServiceListOptions{
|
|
Filters: filters.NewArgs(),
|
|
Status: true,
|
|
}
|
|
opts.Filters.Add("name", name)
|
|
|
|
services, err := p.Client.ServiceList(ctx, opts)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(services) == 0 {
|
|
return nil, fmt.Errorf("service with name %s was not found", name)
|
|
}
|
|
|
|
for _, service := range services {
|
|
// Exact match
|
|
if service.Spec.Name == name {
|
|
return &service, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("service %s was not found because it did not match exactly or on suffix", name)
|
|
}
|
|
|
|
func (p *DockerSwarmProvider) getInstanceName(name string, service swarm.Service) string {
|
|
if name == service.Spec.Name {
|
|
return name
|
|
}
|
|
|
|
return fmt.Sprintf("%s (%s)", name, service.Spec.Name)
|
|
}
|
|
|
|
func (p *DockerSwarmProvider) NotifyInstanceStopped(ctx context.Context, instance chan<- string) {
|
|
msgs, errs := p.Client.Events(ctx, types.EventsOptions{
|
|
Filters: filters.NewArgs(
|
|
filters.Arg("scope", "swarm"),
|
|
filters.Arg("type", "service"),
|
|
),
|
|
})
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case msg, ok := <-msgs:
|
|
if !ok {
|
|
p.l.ErrorContext(ctx, "event stream closed")
|
|
return
|
|
}
|
|
if msg.Actor.Attributes["replicas.new"] == "0" {
|
|
instance <- msg.Actor.Attributes["name"]
|
|
} else if msg.Action == "remove" {
|
|
instance <- msg.Actor.Attributes["name"]
|
|
}
|
|
case err, ok := <-errs:
|
|
if !ok {
|
|
p.l.ErrorContext(ctx, "event stream closed")
|
|
return
|
|
}
|
|
if errors.Is(err, io.EOF) {
|
|
p.l.ErrorContext(ctx, "event stream closed")
|
|
return
|
|
}
|
|
p.l.ErrorContext(ctx, "event stream error", slog.Any("error", err))
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|