-
-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(providers): add
provider.auto-stop-on-startup
argument (#346)
This feature adds the capability to stop unregistered running instances upon startup. Previously, you had to stop running instances manually or issue an initial request that will shut down instances afterwards. With this change, all discovered instances will be shutdown. They need to be registered using labels. E.g.: sablier.enable=true Fixes #153
- Loading branch information
Showing
68 changed files
with
1,116 additions
and
106 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
app/providers/docker_classic_test.go → app/providers/docker/docker_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package providers | ||
package docker | ||
|
||
import ( | ||
"context" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
app/providers/docker_swarm_test.go → ...roviders/dockerswarm/docker_swarm_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package providers | ||
package dockerswarm | ||
|
||
import ( | ||
"context" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
Oops, something went wrong.