Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(providers): add provider.auto-stop-on-startup argument #346

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions app/discovery/autostop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package discovery

import (
"context"
"github.com/acouvreur/sablier/app/providers"
"github.com/acouvreur/sablier/pkg/arrays"
log "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
)

// StopAllUnregisteredInstances stops all auto-discovered running instances that are not yet registered
// as running instances by Sablier.
// By default, Sablier does not stop all already running instances. Meaning that you need to make an
// initial request in order to trigger the scaling to zero.
func StopAllUnregisteredInstances(ctx context.Context, provider providers.Provider, registered []string) error {
log.Info("Stopping all unregistered running instances")

log.Tracef("Retrieving all instances with label [%v=true]", LabelEnable)
instances, err := provider.InstanceList(ctx, providers.InstanceListOptions{
All: false, // Only running containers
Labels: []string{LabelEnable},
})
if err != nil {
return err
}

log.Tracef("Found %v instances with label [%v=true]", len(instances), LabelEnable)
names := make([]string, 0, len(instances))
for _, instance := range instances {
names = append(names, instance.Name)
}

unregistered := arrays.RemoveElements(names, registered)
log.Tracef("Found %v unregistered instances ", len(instances))

waitGroup := errgroup.Group{}

// Previously, the variables declared by a “for” loop were created once and updated by each iteration.
// In Go 1.22, each iteration of the loop creates new variables, to avoid accidental sharing bugs.
// The transition support tooling described in the proposal continues to work in the same way it did in Go 1.21.
for _, name := range unregistered {
waitGroup.Go(stopFunc(ctx, name, provider))
}

return waitGroup.Wait()
}

func stopFunc(ctx context.Context, name string, provider providers.Provider) func() error {
return func() error {
log.Tracef("Stopping %v...", name)
_, err := provider.Stop(ctx, name)
if err != nil {
log.Errorf("Could not stop %v: %v", name, err)
return err
}
log.Tracef("Successfully stopped %v", name)
return nil
}
}
76 changes: 76 additions & 0 deletions app/discovery/autostop_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package discovery_test

import (
"context"
"errors"
"github.com/acouvreur/sablier/app/discovery"
"github.com/acouvreur/sablier/app/instance"
"github.com/acouvreur/sablier/app/providers"
"github.com/acouvreur/sablier/app/providers/mock"
"github.com/acouvreur/sablier/app/types"
"testing"
)

func TestStopAllUnregisteredInstances(t *testing.T) {
mockProvider := new(mock.ProviderMock)
ctx := context.TODO()

// Define instances and registered instances
instances := []types.Instance{
{Name: "instance1"},
{Name: "instance2"},
{Name: "instance3"},
}
registered := []string{"instance1"}

// Set up expectations for InstanceList
mockProvider.On("InstanceList", ctx, providers.InstanceListOptions{
All: false,
Labels: []string{discovery.LabelEnable},
}).Return(instances, nil)

// Set up expectations for Stop
mockProvider.On("Stop", ctx, "instance2").Return(instance.State{}, nil)
mockProvider.On("Stop", ctx, "instance3").Return(instance.State{}, nil)

// Call the function under test
err := discovery.StopAllUnregisteredInstances(ctx, mockProvider, registered)
if err != nil {
t.Fatalf("Expected no error, but got %v", err)
}

// Check expectations
mockProvider.AssertExpectations(t)
}

func TestStopAllUnregisteredInstances_WithError(t *testing.T) {
mockProvider := new(mock.ProviderMock)
ctx := context.TODO()

// Define instances and registered instances
instances := []types.Instance{
{Name: "instance1"},
{Name: "instance2"},
{Name: "instance3"},
}
registered := []string{"instance1"}

// Set up expectations for InstanceList
mockProvider.On("InstanceList", ctx, providers.InstanceListOptions{
All: false,
Labels: []string{discovery.LabelEnable},
}).Return(instances, nil)

// Set up expectations for Stop with error
mockProvider.On("Stop", ctx, "instance2").Return(instance.State{}, errors.New("stop error"))
mockProvider.On("Stop", ctx, "instance3").Return(instance.State{}, nil)

// Call the function under test
err := discovery.StopAllUnregisteredInstances(ctx, mockProvider, registered)
if err == nil {
t.Fatalf("Expected error, but got nil")
}

// Check expectations
mockProvider.AssertExpectations(t)
}
18 changes: 18 additions & 0 deletions app/discovery/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package discovery

const (
LabelEnable = "sablier.enable"
LabelGroup = "sablier.group"
LabelGroupDefaultValue = "default"
LabelReplicas = "sablier.replicas"
LabelReplicasDefaultValue uint64 = 1
)

type Group struct {
Name string
Instances []Instance
}

type Instance struct {
Name string
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package providers
package docker

import (
"context"
"errors"
"fmt"
"github.com/acouvreur/sablier/app/discovery"
"io"
"strings"

Expand Down Expand Up @@ -33,7 +34,7 @@ func NewDockerClassicProvider() (*DockerClassicProvider, error) {
return nil, fmt.Errorf("cannot connect to docker host: %v", err)
}

log.Trace(fmt.Sprintf("connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion))
log.Tracef("connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion)

return &DockerClassicProvider{
Client: cli,
Expand All @@ -43,7 +44,7 @@ func NewDockerClassicProvider() (*DockerClassicProvider, error) {

func (provider *DockerClassicProvider) GetGroups(ctx context.Context) (map[string][]string, error) {
args := filters.NewArgs()
args.Add("label", fmt.Sprintf("%s=true", enableLabel))
args.Add("label", fmt.Sprintf("%s=true", discovery.LabelEnable))

containers, err := provider.Client.ContainerList(ctx, container.ListOptions{
All: true,
Expand All @@ -56,9 +57,9 @@ func (provider *DockerClassicProvider) GetGroups(ctx context.Context) (map[strin

groups := make(map[string][]string)
for _, c := range containers {
groupName := c.Labels[groupLabel]
groupName := c.Labels[discovery.LabelGroup]
if len(groupName) == 0 {
groupName = defaultGroupValue
groupName = discovery.LabelGroupDefaultValue
}
group := groups[groupName]
group = append(group, strings.TrimPrefix(c.Names[0], "/"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package providers
package docker

import (
"context"
Expand Down
60 changes: 60 additions & 0 deletions app/providers/docker/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package docker

import (
"context"
"fmt"
"github.com/acouvreur/sablier/app/discovery"
"github.com/acouvreur/sablier/app/providers"
"github.com/acouvreur/sablier/app/types"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"strings"
)

func (provider *DockerClassicProvider) InstanceList(ctx context.Context, options providers.InstanceListOptions) ([]types.Instance, error) {
args := filters.NewArgs()
for _, label := range options.Labels {
args.Add("label", label)
args.Add("label", fmt.Sprintf("%s=true", label))
}

containers, err := provider.Client.ContainerList(ctx, container.ListOptions{
All: options.All,
Filters: args,
})

if err != nil {
return nil, err
}

instances := make([]types.Instance, 0, len(containers))
for _, c := range containers {
instance := containerToInstance(c)
instances = append(instances, instance)
}

return instances, nil
}

func containerToInstance(c dockertypes.Container) types.Instance {
var group string

if _, ok := c.Labels[discovery.LabelEnable]; ok {
if g, ok := c.Labels[discovery.LabelGroup]; ok {
group = g
} else {
group = discovery.LabelGroupDefaultValue
}
}

return types.Instance{
Name: strings.TrimPrefix(c.Names[0], "/"), // Containers name are reported with a leading slash
Kind: "container",
Status: c.Status,
// Replicas: c.Status,
// DesiredReplicas: 1,
ScalingReplicas: 1,
Group: group,
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package providers
package dockerswarm

import (
"context"
"errors"
"fmt"
"github.com/acouvreur/sablier/app/discovery"
"io"
"strings"

Expand Down Expand Up @@ -78,7 +79,7 @@ func (provider *DockerSwarmProvider) scale(ctx context.Context, name string, rep

func (provider *DockerSwarmProvider) GetGroups(ctx context.Context) (map[string][]string, error) {
filters := filters.NewArgs()
filters.Add("label", fmt.Sprintf("%s=true", enableLabel))
filters.Add("label", fmt.Sprintf("%s=true", discovery.LabelEnable))

services, err := provider.Client.ServiceList(ctx, types.ServiceListOptions{
Filters: filters,
Expand All @@ -90,9 +91,9 @@ func (provider *DockerSwarmProvider) GetGroups(ctx context.Context) (map[string]

groups := make(map[string][]string)
for _, service := range services {
groupName := service.Spec.Labels[groupLabel]
groupName := service.Spec.Labels[discovery.LabelGroup]
if len(groupName) == 0 {
groupName = defaultGroupValue
groupName = discovery.LabelGroupDefaultValue
}

group := groups[groupName]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package providers
package dockerswarm

import (
"context"
Expand Down
74 changes: 74 additions & 0 deletions app/providers/dockerswarm/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package dockerswarm

import (
"context"
"fmt"
"github.com/acouvreur/sablier/app/discovery"
"github.com/acouvreur/sablier/app/providers"
"github.com/acouvreur/sablier/app/types"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
log "github.com/sirupsen/logrus"
"strconv"
)

func (provider *DockerSwarmProvider) InstanceList(ctx context.Context, options providers.InstanceListOptions) ([]types.Instance, error) {
args := filters.NewArgs()
for _, label := range options.Labels {
args.Add("label", label)
args.Add("label", fmt.Sprintf("%s=true", label))
}

services, err := provider.Client.ServiceList(ctx, dockertypes.ServiceListOptions{
Filters: args,
})

if err != nil {
return nil, err
}

instances := make([]types.Instance, 0, len(services))
for _, s := range services {
instance := serviceToInstance(s)
instances = append(instances, instance)
}

return instances, nil
}

func serviceToInstance(s swarm.Service) (i types.Instance) {
var group string
var replicas uint64

if _, ok := s.Spec.Labels[discovery.LabelEnable]; ok {
if g, ok := s.Spec.Labels[discovery.LabelGroup]; ok {
group = g
} else {
group = discovery.LabelGroupDefaultValue
}

if r, ok := s.Spec.Labels[discovery.LabelReplicas]; ok {
atoi, err := strconv.Atoi(r)
if err != nil {
log.Warnf("Defaulting to default replicas value, could not convert value \"%v\" to int: %v", r, err)
replicas = discovery.LabelReplicasDefaultValue
} else {
replicas = uint64(atoi)
}
} else {
replicas = discovery.LabelReplicasDefaultValue
}
}

return types.Instance{
Name: s.Spec.Name,
Kind: "service",
// TODO
// Status: string(s.UpdateStatus.State),
// Replicas: s.ServiceStatus.RunningTasks,
// DesiredReplicas: s.ServiceStatus.DesiredTasks,
ScalingReplicas: replicas,
Group: group,
}
}
Loading
Loading