diff --git a/app/discovery/autostop.go b/app/discovery/autostop.go new file mode 100644 index 00000000..390ead11 --- /dev/null +++ b/app/discovery/autostop.go @@ -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 + } +} diff --git a/app/discovery/autostop_test.go b/app/discovery/autostop_test.go new file mode 100644 index 00000000..ef6dcb63 --- /dev/null +++ b/app/discovery/autostop_test.go @@ -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) +} diff --git a/app/discovery/types.go b/app/discovery/types.go new file mode 100644 index 00000000..f5eb8b5c --- /dev/null +++ b/app/discovery/types.go @@ -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 +} diff --git a/app/providers/docker_classic.go b/app/providers/docker/docker.go similarity index 93% rename from app/providers/docker_classic.go rename to app/providers/docker/docker.go index 5b37d37e..97979f3c 100644 --- a/app/providers/docker_classic.go +++ b/app/providers/docker/docker.go @@ -1,9 +1,10 @@ -package providers +package docker import ( "context" "errors" "fmt" + "github.com/acouvreur/sablier/app/discovery" "io" "strings" @@ -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, @@ -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, @@ -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], "/")) diff --git a/app/providers/docker_classic_test.go b/app/providers/docker/docker_test.go similarity index 99% rename from app/providers/docker_classic_test.go rename to app/providers/docker/docker_test.go index 82587234..7f5afe82 100644 --- a/app/providers/docker_classic_test.go +++ b/app/providers/docker/docker_test.go @@ -1,4 +1,4 @@ -package providers +package docker import ( "context" diff --git a/app/providers/docker/list.go b/app/providers/docker/list.go new file mode 100644 index 00000000..4a453085 --- /dev/null +++ b/app/providers/docker/list.go @@ -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, + } +} diff --git a/app/providers/docker_swarm.go b/app/providers/dockerswarm/docker_swarm.go similarity index 95% rename from app/providers/docker_swarm.go rename to app/providers/dockerswarm/docker_swarm.go index 029f23d3..160c01fd 100644 --- a/app/providers/docker_swarm.go +++ b/app/providers/dockerswarm/docker_swarm.go @@ -1,9 +1,10 @@ -package providers +package dockerswarm import ( "context" "errors" "fmt" + "github.com/acouvreur/sablier/app/discovery" "io" "strings" @@ -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, @@ -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] diff --git a/app/providers/docker_swarm_test.go b/app/providers/dockerswarm/docker_swarm_test.go similarity index 99% rename from app/providers/docker_swarm_test.go rename to app/providers/dockerswarm/docker_swarm_test.go index dce3cc35..108cd912 100644 --- a/app/providers/docker_swarm_test.go +++ b/app/providers/dockerswarm/docker_swarm_test.go @@ -1,4 +1,4 @@ -package providers +package dockerswarm import ( "context" diff --git a/app/providers/dockerswarm/list.go b/app/providers/dockerswarm/list.go new file mode 100644 index 00000000..50ecfbb7 --- /dev/null +++ b/app/providers/dockerswarm/list.go @@ -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, + } +} diff --git a/app/providers/kubernetes.go b/app/providers/kubernetes/kubernetes.go similarity index 85% rename from app/providers/kubernetes.go rename to app/providers/kubernetes/kubernetes.go index d32b63a4..2abb2f20 100644 --- a/app/providers/kubernetes.go +++ b/app/providers/kubernetes/kubernetes.go @@ -1,9 +1,10 @@ -package providers +package kubernetes import ( "context" "errors" "fmt" + "github.com/acouvreur/sablier/app/discovery" "strconv" "strings" "time" @@ -54,15 +55,6 @@ func (provider *KubernetesProvider) convertName(name string) (*Config, error) { }, nil } -func (provider *KubernetesProvider) convertStatefulset(ss *appsv1.StatefulSet, replicas int32) string { - return fmt.Sprintf("statefulset%s%s%s%s%s%d", provider.delimiter, ss.Namespace, provider.delimiter, ss.Name, provider.delimiter, replicas) -} - -func (provider *KubernetesProvider) convertDeployment(d *appsv1.Deployment, replicas int32) string { - return fmt.Sprintf("deployment%s%s%s%s%s%d", provider.delimiter, d.Namespace, provider.delimiter, d.Name, provider.delimiter, replicas) - -} - type KubernetesProvider struct { Client kubernetes.Interface delimiter string @@ -112,7 +104,7 @@ func (provider *KubernetesProvider) Stop(ctx context.Context, name string) (inst func (provider *KubernetesProvider) GetGroups(ctx context.Context) (map[string][]string, error) { deployments, err := provider.Client.AppsV1().Deployments(core_v1.NamespaceAll).List(ctx, metav1.ListOptions{ - LabelSelector: enableLabel, + LabelSelector: discovery.LabelEnable, }) if err != nil { @@ -121,20 +113,19 @@ func (provider *KubernetesProvider) GetGroups(ctx context.Context) (map[string][ groups := make(map[string][]string) for _, deployment := range deployments.Items { - groupName := deployment.Labels[groupLabel] + groupName := deployment.Labels[discovery.LabelGroup] if len(groupName) == 0 { - groupName = defaultGroupValue + groupName = discovery.LabelGroupDefaultValue } group := groups[groupName] - // TOOD: Use annotation for scale - name := provider.convertDeployment(&deployment, 1) - group = append(group, name) + parsed := DeploymentName(deployment, ParseOptions{Delimiter: provider.delimiter}) + group = append(group, parsed.Original) groups[groupName] = group } statefulSets, err := provider.Client.AppsV1().StatefulSets(core_v1.NamespaceAll).List(ctx, metav1.ListOptions{ - LabelSelector: enableLabel, + LabelSelector: discovery.LabelEnable, }) if err != nil { @@ -142,15 +133,14 @@ func (provider *KubernetesProvider) GetGroups(ctx context.Context) (map[string][ } for _, statefulSet := range statefulSets.Items { - groupName := statefulSet.Labels[groupLabel] + groupName := statefulSet.Labels[discovery.LabelGroup] if len(groupName) == 0 { - groupName = defaultGroupValue + groupName = discovery.LabelGroupDefaultValue } group := groups[groupName] - // TOOD: Use annotation for scale - name := provider.convertStatefulset(&statefulSet, 1) - group = append(group, name) + parsed := StatefulSetName(statefulSet, ParseOptions{Delimiter: provider.delimiter}) + group = append(group, parsed.Original) groups[groupName] = group } @@ -249,12 +239,14 @@ func (provider *KubernetesProvider) watchDeployents(instance chan<- string) cach } if *newDeployment.Spec.Replicas == 0 { - instance <- provider.convertDeployment(newDeployment, *oldDeployment.Spec.Replicas) + parsed := DeploymentName(*newDeployment, ParseOptions{Delimiter: provider.delimiter}) + instance <- parsed.Original } }, DeleteFunc: func(obj interface{}) { deletedDeployment := obj.(*appsv1.Deployment) - instance <- provider.convertDeployment(deletedDeployment, *deletedDeployment.Spec.Replicas) + parsed := DeploymentName(*deletedDeployment, ParseOptions{Delimiter: provider.delimiter}) + instance <- parsed.Original }, } factory := informers.NewSharedInformerFactoryWithOptions(provider.Client, 2*time.Second, informers.WithNamespace(core_v1.NamespaceAll)) @@ -275,12 +267,14 @@ func (provider *KubernetesProvider) watchStatefulSets(instance chan<- string) ca } if *newStatefulSet.Spec.Replicas == 0 { - instance <- provider.convertStatefulset(newStatefulSet, *oldStatefulSet.Spec.Replicas) + parsed := StatefulSetName(*newStatefulSet, ParseOptions{Delimiter: provider.delimiter}) + instance <- parsed.Original } }, DeleteFunc: func(obj interface{}) { deletedStatefulSet := obj.(*appsv1.StatefulSet) - instance <- provider.convertStatefulset(deletedStatefulSet, *deletedStatefulSet.Spec.Replicas) + parsed := StatefulSetName(*deletedStatefulSet, ParseOptions{Delimiter: provider.delimiter}) + instance <- parsed.Original }, } factory := informers.NewSharedInformerFactoryWithOptions(provider.Client, 2*time.Second, informers.WithNamespace(core_v1.NamespaceAll)) diff --git a/app/providers/kubernetes_test.go b/app/providers/kubernetes/kubernetes_test.go similarity index 99% rename from app/providers/kubernetes_test.go rename to app/providers/kubernetes/kubernetes_test.go index 1b571a4c..c21d9dd9 100644 --- a/app/providers/kubernetes_test.go +++ b/app/providers/kubernetes/kubernetes_test.go @@ -1,4 +1,4 @@ -package providers +package kubernetes import ( "context" diff --git a/app/providers/kubernetes/list.go b/app/providers/kubernetes/list.go new file mode 100644 index 00000000..86b2254d --- /dev/null +++ b/app/providers/kubernetes/list.go @@ -0,0 +1,138 @@ +package kubernetes + +import ( + "context" + "github.com/acouvreur/sablier/app/discovery" + "github.com/acouvreur/sablier/app/providers" + "github.com/acouvreur/sablier/app/types" + log "github.com/sirupsen/logrus" + v1 "k8s.io/api/apps/v1" + core_v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "strconv" + "strings" +) + +func (provider *KubernetesProvider) InstanceList(ctx context.Context, options providers.InstanceListOptions) ([]types.Instance, error) { + deployments, err := provider.deploymentList(ctx, options) + if err != nil { + return nil, err + } + + statefulSets, err := provider.statefulSetList(ctx, options) + if err != nil { + return nil, err + } + + return append(deployments, statefulSets...), nil +} + +func (provider *KubernetesProvider) deploymentList(ctx context.Context, options providers.InstanceListOptions) ([]types.Instance, error) { + deployments, err := provider.Client.AppsV1().Deployments(core_v1.NamespaceAll).List(ctx, metav1.ListOptions{ + LabelSelector: strings.Join(options.Labels, ","), + }) + + if err != nil { + return nil, err + } + + instances := make([]types.Instance, 0, len(deployments.Items)) + for _, d := range deployments.Items { + instance := provider.deploymentToInstance(d) + instances = append(instances, instance) + } + + return instances, nil +} + +func (provider *KubernetesProvider) deploymentToInstance(d v1.Deployment) types.Instance { + var group string + var replicas uint64 + + if _, ok := d.Labels[discovery.LabelEnable]; ok { + if g, ok := d.Labels[discovery.LabelGroup]; ok { + group = g + } else { + group = discovery.LabelGroupDefaultValue + } + + if r, ok := d.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 + } + } + + parsed := DeploymentName(d, ParseOptions{Delimiter: provider.delimiter}) + + return types.Instance{ + Name: parsed.Original, + Kind: parsed.Kind, + Status: d.Status.String(), + Replicas: uint64(d.Status.Replicas), + DesiredReplicas: uint64(*d.Spec.Replicas), + ScalingReplicas: replicas, + Group: group, + } +} + +func (provider *KubernetesProvider) statefulSetList(ctx context.Context, options providers.InstanceListOptions) ([]types.Instance, error) { + statefulSets, err := provider.Client.AppsV1().StatefulSets(core_v1.NamespaceAll).List(ctx, metav1.ListOptions{ + LabelSelector: strings.Join(options.Labels, ","), + }) + + if err != nil { + return nil, err + } + + instances := make([]types.Instance, 0, len(statefulSets.Items)) + for _, ss := range statefulSets.Items { + instance := provider.statefulSetToInstance(ss) + instances = append(instances, instance) + } + + return instances, nil +} + +func (provider *KubernetesProvider) statefulSetToInstance(ss v1.StatefulSet) types.Instance { + var group string + var replicas uint64 + + if _, ok := ss.Labels[discovery.LabelEnable]; ok { + if g, ok := ss.Labels[discovery.LabelGroup]; ok { + group = g + } else { + group = discovery.LabelGroupDefaultValue + } + + if r, ok := ss.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 + } + } + + parsed := StatefulSetName(ss, ParseOptions{Delimiter: provider.delimiter}) + + return types.Instance{ + Name: parsed.Original, + Kind: parsed.Kind, + Status: ss.Status.String(), + Replicas: uint64(ss.Status.Replicas), + DesiredReplicas: uint64(*ss.Spec.Replicas), + ScalingReplicas: replicas, + Group: group, + } +} diff --git a/app/providers/kubernetes/parse_name.go b/app/providers/kubernetes/parse_name.go new file mode 100644 index 00000000..3f5bacc7 --- /dev/null +++ b/app/providers/kubernetes/parse_name.go @@ -0,0 +1,64 @@ +package kubernetes + +import ( + "fmt" + "strings" + + v1 "k8s.io/api/apps/v1" +) + +type ParsedName struct { + Original string + Kind string // deployment or statefulset + Namespace string + Name string +} + +type ParseOptions struct { + Delimiter string +} + +func ParseName(name string, opts ParseOptions) (ParsedName, error) { + + split := strings.Split(name, opts.Delimiter) + if len(split) < 3 { + return ParsedName{}, fmt.Errorf("invalid name should be: kind%snamespace%sname (have %s)", opts.Delimiter, opts.Delimiter, name) + } + + return ParsedName{ + Original: name, + Kind: split[0], + Namespace: split[1], + Name: split[2], + }, nil +} + +func DeploymentName(deployment v1.Deployment, opts ParseOptions) ParsedName { + kind := "deployment" + namespace := deployment.Namespace + name := deployment.Name + // TOOD: Use annotation for scale + original := fmt.Sprintf("%s%s%s%s%s%s%d", kind, opts.Delimiter, namespace, opts.Delimiter, name, opts.Delimiter, 1) + + return ParsedName{ + Original: original, + Kind: kind, + Namespace: namespace, + Name: name, + } +} + +func StatefulSetName(statefulSet v1.StatefulSet, opts ParseOptions) ParsedName { + kind := "statefulset" + namespace := statefulSet.Namespace + name := statefulSet.Name + // TOOD: Use annotation for scale + original := fmt.Sprintf("%s%s%s%s%s%s%d", kind, opts.Delimiter, namespace, opts.Delimiter, name, opts.Delimiter, 1) + + return ParsedName{ + Original: original, + Kind: kind, + Namespace: namespace, + Name: name, + } +} diff --git a/app/providers/kubernetes/parse_name_test.go b/app/providers/kubernetes/parse_name_test.go new file mode 100644 index 00000000..d7bf71ea --- /dev/null +++ b/app/providers/kubernetes/parse_name_test.go @@ -0,0 +1,106 @@ +package kubernetes + +import ( + v1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" +) + +func TestParseName(t *testing.T) { + tests := []struct { + name string + input string + opts ParseOptions + expected ParsedName + hasError bool + }{ + { + name: "Valid name with default delimiter", + input: "deployment:namespace:name", + opts: ParseOptions{Delimiter: ":"}, + expected: ParsedName{Original: "deployment:namespace:name", Kind: "deployment", Namespace: "namespace", Name: "name"}, + hasError: false, + }, + { + name: "Invalid name with missing parts", + input: "deployment:namespace", + opts: ParseOptions{Delimiter: ":"}, + expected: ParsedName{}, + hasError: true, + }, + { + name: "Valid name with custom delimiter", + input: "statefulset#namespace#name", + opts: ParseOptions{Delimiter: "#"}, + expected: ParsedName{Original: "statefulset#namespace#name", Kind: "statefulset", Namespace: "namespace", Name: "name"}, + hasError: false, + }, + { + name: "Invalid name with incorrect delimiter", + input: "statefulset:namespace:name", + opts: ParseOptions{Delimiter: "#"}, + expected: ParsedName{}, + hasError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ParseName(tt.input, tt.opts) + if tt.hasError { + if err == nil { + t.Errorf("expected error but got nil") + } + } else { + if err != nil { + t.Errorf("expected no error but got %v", err) + } + if result != tt.expected { + t.Errorf("expected %v but got %v", tt.expected, result) + } + } + }) + } +} + +func TestDeploymentName(t *testing.T) { + deployment := v1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-deployment", + }, + } + opts := ParseOptions{Delimiter: ":"} + expected := ParsedName{ + Original: "deployment:test-namespace:test-deployment:1", + Kind: "deployment", + Namespace: "test-namespace", + Name: "test-deployment", + } + + result := DeploymentName(deployment, opts) + if result != expected { + t.Errorf("expected %v but got %v", expected, result) + } +} + +func TestStatefulSetName(t *testing.T) { + statefulSet := v1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-statefulset", + }, + } + opts := ParseOptions{Delimiter: ":"} + expected := ParsedName{ + Original: "statefulset:test-namespace:test-statefulset:1", + Kind: "statefulset", + Namespace: "test-namespace", + Name: "test-statefulset", + } + + result := StatefulSetName(statefulSet, opts) + if result != expected { + t.Errorf("expected %v but got %v", expected, result) + } +} diff --git a/app/providers/mock/mock.go b/app/providers/mock/mock.go new file mode 100644 index 00000000..4d8bd232 --- /dev/null +++ b/app/providers/mock/mock.go @@ -0,0 +1,39 @@ +package mock + +import ( + "context" + "github.com/acouvreur/sablier/app/instance" + "github.com/acouvreur/sablier/app/providers" + "github.com/acouvreur/sablier/app/types" + "github.com/stretchr/testify/mock" +) + +// ProviderMock is a structure that allows to define the behavior of a Provider +type ProviderMock struct { + mock.Mock +} + +func (m *ProviderMock) Start(ctx context.Context, name string) (instance.State, error) { + args := m.Called(ctx, name) + return args.Get(0).(instance.State), args.Error(1) +} +func (m *ProviderMock) Stop(ctx context.Context, name string) (instance.State, error) { + args := m.Called(ctx, name) + return args.Get(0).(instance.State), args.Error(1) +} +func (m *ProviderMock) GetState(ctx context.Context, name string) (instance.State, error) { + args := m.Called(ctx, name) + return args.Get(0).(instance.State), args.Error(1) +} +func (m *ProviderMock) GetGroups(ctx context.Context) (map[string][]string, error) { + args := m.Called(ctx) + return args.Get(0).(map[string][]string), args.Error(1) +} +func (m *ProviderMock) InstanceList(ctx context.Context, options providers.InstanceListOptions) ([]types.Instance, error) { + args := m.Called(ctx, options) + return args.Get(0).([]types.Instance), args.Error(1) +} + +func (m *ProviderMock) NotifyInstanceStopped(ctx context.Context, instance chan<- string) { + m.Called(ctx, instance) +} diff --git a/app/providers/provider.go b/app/providers/provider.go index c7e49a6a..89ac3d05 100644 --- a/app/providers/provider.go +++ b/app/providers/provider.go @@ -2,37 +2,17 @@ package providers import ( "context" - "fmt" + "github.com/acouvreur/sablier/app/types" "github.com/acouvreur/sablier/app/instance" - "github.com/acouvreur/sablier/config" ) -const enableLabel = "sablier.enable" -const groupLabel = "sablier.group" -const defaultGroupValue = "default" - type Provider interface { Start(ctx context.Context, name string) (instance.State, error) Stop(ctx context.Context, name string) (instance.State, error) GetState(ctx context.Context, name string) (instance.State, error) GetGroups(ctx context.Context) (map[string][]string, error) + InstanceList(ctx context.Context, options InstanceListOptions) ([]types.Instance, error) NotifyInstanceStopped(ctx context.Context, instance chan<- string) } - -func NewProvider(config config.Provider) (Provider, error) { - if err := config.IsValid(); err != nil { - return nil, err - } - - switch config.Name { - case "swarm", "docker_swarm": - return NewDockerSwarmProvider() - case "docker": - return NewDockerClassicProvider() - case "kubernetes": - return NewKubernetesProvider(config.Kubernetes) - } - return nil, fmt.Errorf("unimplemented provider %s", config.Name) -} diff --git a/app/providers/types.go b/app/providers/types.go new file mode 100644 index 00000000..975faa46 --- /dev/null +++ b/app/providers/types.go @@ -0,0 +1,6 @@ +package providers + +type InstanceListOptions struct { + All bool + Labels []string +} diff --git a/app/sablier.go b/app/sablier.go index 748e8744..08f05182 100644 --- a/app/sablier.go +++ b/app/sablier.go @@ -2,6 +2,11 @@ package app import ( "context" + "fmt" + "github.com/acouvreur/sablier/app/discovery" + "github.com/acouvreur/sablier/app/providers/docker" + "github.com/acouvreur/sablier/app/providers/dockerswarm" + "github.com/acouvreur/sablier/app/providers/kubernetes" "os" "github.com/acouvreur/sablier/app/http" @@ -29,7 +34,7 @@ func Start(conf config.Config) error { log.Info(version.Info()) - provider, err := providers.NewProvider(conf.Provider) + provider, err := NewProvider(conf.Provider) if err != nil { return err } @@ -51,6 +56,13 @@ func Start(conf config.Config) error { loadSessions(storage, sessionsManager) } + if conf.Provider.AutoStopOnStartup { + err := discovery.StopAllUnregisteredInstances(context.Background(), provider, store.Keys()) + if err != nil { + log.Warnf("Stopping unregistered instances had an error: %v", err) + } + } + var t *theme.Themes if conf.Strategy.Dynamic.CustomThemesPath != "" { @@ -110,3 +122,19 @@ func saveSessions(storage storage.Storage, sessions sessions.Manager) { log.Error("error saving sessions", err) } } + +func NewProvider(config config.Provider) (providers.Provider, error) { + if err := config.IsValid(); err != nil { + return nil, err + } + + switch config.Name { + case "swarm", "docker_swarm": + return dockerswarm.NewDockerSwarmProvider() + case "docker": + return docker.NewDockerClassicProvider() + case "kubernetes": + return kubernetes.NewKubernetesProvider(config.Kubernetes) + } + return nil, fmt.Errorf("unimplemented provider %s", config.Name) +} diff --git a/app/types/instance.go b/app/types/instance.go new file mode 100644 index 00000000..858fd57c --- /dev/null +++ b/app/types/instance.go @@ -0,0 +1,11 @@ +package types + +type Instance struct { + Name string + Kind string + Status string + Replicas uint64 + DesiredReplicas uint64 + ScalingReplicas uint64 + Group string +} diff --git a/app/types/session.go b/app/types/session.go new file mode 100644 index 00000000..ab1254f4 --- /dev/null +++ b/app/types/session.go @@ -0,0 +1 @@ +package types diff --git a/cmd/root.go b/cmd/root.go index 986423b7..d205c142 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -46,6 +46,8 @@ It provides an integrations with multiple reverse proxies and different loading // Provider flags startCmd.Flags().StringVar(&conf.Provider.Name, "provider.name", "docker", fmt.Sprintf("Provider to use to manage containers %v", config.GetProviders())) viper.BindPFlag("provider.name", startCmd.Flags().Lookup("provider.name")) + startCmd.Flags().BoolVar(&conf.Provider.AutoStopOnStartup, "provider.auto-stop-on-startup", true, "") + viper.BindPFlag("provider.auto-stop-on-startup", startCmd.Flags().Lookup("provider.auto-stop-on-startup")) startCmd.Flags().Float32Var(&conf.Provider.Kubernetes.QPS, "provider.kubernetes.qps", 5, "QPS limit for K8S API access client-side throttling") viper.BindPFlag("provider.kubernetes.qps", startCmd.Flags().Lookup("provider.kubernetes.qps")) startCmd.Flags().IntVar(&conf.Provider.Kubernetes.Burst, "provider.kubernetes.burst", 10, "Maximum burst for K8S API acees client-side throttling") diff --git a/cmd/root_test.go b/cmd/root_test.go index f9181760..0b7d4244 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "encoding/json" - "io/ioutil" "os" "path/filepath" "strings" @@ -21,7 +20,7 @@ func TestDefault(t *testing.T) { testDir, err := os.Getwd() require.NoError(t, err, "error getting the current working directory") - wantConfig, err := ioutil.ReadFile(filepath.Join(testDir, "testdata", "config_default.json")) + wantConfig, err := os.ReadFile(filepath.Join(testDir, "testdata", "config_default.json")) require.NoError(t, err, "error reading test config file") // CHANGE `startCmd` behavior to only print the config, this is for testing purposes only @@ -51,7 +50,7 @@ func TestPrecedence(t *testing.T) { newStartCommand = mockStartCommand t.Run("config file", func(t *testing.T) { - wantConfig, err := ioutil.ReadFile(filepath.Join(testDir, "testdata", "config_yaml_wanted.json")) + wantConfig, err := os.ReadFile(filepath.Join(testDir, "testdata", "config_yaml_wanted.json")) require.NoError(t, err, "error reading test config file") conf = config.NewConfig() @@ -95,7 +94,7 @@ func TestPrecedence(t *testing.T) { setEnvsFromFile(filepath.Join(testDir, "testdata", "config.env")) defer unsetEnvsFromFile(filepath.Join(testDir, "testdata", "config.env")) - wantConfig, err := ioutil.ReadFile(filepath.Join(testDir, "testdata", "config_cli_wanted.json")) + wantConfig, err := os.ReadFile(filepath.Join(testDir, "testdata", "config_cli_wanted.json")) require.NoError(t, err, "error reading test config file") cmd := NewRootCommand() diff --git a/cmd/testdata/config.env b/cmd/testdata/config.env index 11fef976..8a240fa8 100644 --- a/cmd/testdata/config.env +++ b/cmd/testdata/config.env @@ -1,4 +1,5 @@ PROVIDER_NAME=envvar +PROVIDER_AUTOSTOPONSTARTUP=false PROVIDER_KUBERNETES_QPS=16 PROVIDER_KUBERNETES_BURST=32 PROVIDER_KUBERNETES_DELIMITER=/ diff --git a/cmd/testdata/config.yml b/cmd/testdata/config.yml index e52299e2..3bd73f6f 100644 --- a/cmd/testdata/config.yml +++ b/cmd/testdata/config.yml @@ -1,5 +1,6 @@ provider: name: configfile + auto-stop-on-startup: false kubernetes: qps: 64 burst: 128 diff --git a/cmd/testdata/config_cli_wanted.json b/cmd/testdata/config_cli_wanted.json index 2829f8ed..5904fe67 100644 --- a/cmd/testdata/config_cli_wanted.json +++ b/cmd/testdata/config_cli_wanted.json @@ -8,6 +8,7 @@ }, "Provider": { "Name": "cli", + "AutoStopOnStartup": false, "Kubernetes": { "QPS": 256, "Burst": 512, diff --git a/cmd/testdata/config_default.json b/cmd/testdata/config_default.json index 4bf83ade..ab0d0039 100644 --- a/cmd/testdata/config_default.json +++ b/cmd/testdata/config_default.json @@ -8,6 +8,7 @@ }, "Provider": { "Name": "docker", + "AutoStopOnStartup": true, "Kubernetes": { "QPS": 5, "Burst": 10, diff --git a/cmd/testdata/config_env_wanted.json b/cmd/testdata/config_env_wanted.json index ef6ca0f2..9e9dd64c 100644 --- a/cmd/testdata/config_env_wanted.json +++ b/cmd/testdata/config_env_wanted.json @@ -8,6 +8,7 @@ }, "Provider": { "Name": "envvar", + "AutoStopOnStartup": false, "Kubernetes": { "QPS": 16, "Burst": 32, diff --git a/cmd/testdata/config_yaml_wanted.json b/cmd/testdata/config_yaml_wanted.json index 8262d4c9..9b7a682b 100644 --- a/cmd/testdata/config_yaml_wanted.json +++ b/cmd/testdata/config_yaml_wanted.json @@ -8,6 +8,7 @@ }, "Provider": { "Name": "configfile", + "AutoStopOnStartup": false, "Kubernetes": { "QPS": 64, "Burst": 128, diff --git a/config/provider.go b/config/provider.go index 5cbeaf20..398fc32b 100644 --- a/config/provider.go +++ b/config/provider.go @@ -8,8 +8,9 @@ import ( type Provider struct { // The provider name to use // It can be either docker, swarm or kubernetes. Defaults to "docker" - Name string `mapstructure:"NAME" yaml:"provider,omitempty" default:"docker"` - Kubernetes Kubernetes + Name string `mapstructure:"NAME" yaml:"name,omitempty" default:"docker"` + AutoStopOnStartup bool `yaml:"auto-stop-on-startup,omitempty" default:"true"` + Kubernetes Kubernetes } type Kubernetes struct { diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 8fc4d35d..c799e150 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -137,3 +137,35 @@ func Test_Healthy(t *testing.T) { Status(http.StatusNotFound). Body().Contains(`nginx/`) } + +func Test_Group(t *testing.T) { + e := httpexpect.Default(t, "http://localhost:8080/") + + e.GET("/group"). + Expect(). + Status(http.StatusOK). + Body(). + Contains(`Group E2E`). + Contains(`Your instance(s) will stop after 1 minute of inactivity`) + + e.GET("/group"). + WithMaxRetries(10). + WithRetryDelay(time.Second, time.Second*2). + WithRetryPolicy(httpexpect.RetryCustomHandler). + WithCustomHandler(func(resp *http.Response, _ error) bool { + if resp.Body != nil { + + // Check body if available, etc. + body, err := io.ReadAll(resp.Body) + defer resp.Body.Close() + if err != nil { + return true + } + return !strings.Contains(string(body), "Host: localhost:8080") + } + return false + }). + Expect(). + Status(http.StatusOK). + Body().Contains(`Host: localhost:8080`) +} diff --git a/go.mod b/go.mod index 3367babf..307dc0f6 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 + golang.org/x/sync v0.6.0 gotest.tools/v3 v3.5.1 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 diff --git a/go.sum b/go.sum index 16a44a90..748d9099 100644 --- a/go.sum +++ b/go.sum @@ -29,10 +29,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= -github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v27.0.2+incompatible h1:mNhCtgXNV1fIRns102grG7rdzIsGGCq1OlOD0KunZos= -github.com/docker/docker v27.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= diff --git a/go.work.sum b/go.work.sum index f91a952b..f1ba40cb 100644 --- a/go.work.sum +++ b/go.work.sum @@ -339,18 +339,35 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= diff --git a/pkg/arrays/remove_elements.go b/pkg/arrays/remove_elements.go new file mode 100644 index 00000000..8e1afee3 --- /dev/null +++ b/pkg/arrays/remove_elements.go @@ -0,0 +1,21 @@ +package arrays + +// RemoveElements returns a new slice containing all elements from `allElements` that are not in `elementsToRemove` +func RemoveElements(allElements, elementsToRemove []string) []string { + // Create a map to store elements to remove for quick lookup + removeMap := make(map[string]struct{}, len(elementsToRemove)) + for _, elem := range elementsToRemove { + removeMap[elem] = struct{}{} + } + + // Create a slice to store the result + result := make([]string, 0, len(allElements)) // Preallocate memory based on the size of allElements + for _, elem := range allElements { + // Check if the element is not in the removeMap + if _, found := removeMap[elem]; !found { + result = append(result, elem) + } + } + + return result +} diff --git a/pkg/arrays/remove_elements_test.go b/pkg/arrays/remove_elements_test.go new file mode 100644 index 00000000..7ec26cae --- /dev/null +++ b/pkg/arrays/remove_elements_test.go @@ -0,0 +1,30 @@ +package arrays + +import ( + "reflect" + "testing" +) + +func TestRemoveElements(t *testing.T) { + tests := []struct { + allElements []string + elementsToRemove []string + expected []string + }{ + {[]string{"apple", "banana", "cherry", "date", "fig", "grape"}, []string{"banana", "date", "grape"}, []string{"apple", "cherry", "fig"}}, + {[]string{"apple", "banana", "cherry"}, []string{"date", "fig", "grape"}, []string{"apple", "banana", "cherry"}}, // No elements to remove are present + {[]string{"apple", "banana", "cherry", "date"}, []string{}, []string{"apple", "banana", "cherry", "date"}}, // No elements to remove + {[]string{}, []string{"apple", "banana", "cherry"}, []string{}}, // Empty allElements slice + {[]string{"apple", "banana", "banana", "cherry", "cherry", "date"}, []string{"banana", "cherry"}, []string{"apple", "date"}}, // Duplicate elements in allElements + {[]string{"apple", "apple", "apple", "apple"}, []string{"apple"}, []string{}}, // All elements are removed + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := RemoveElements(tt.allElements, tt.elementsToRemove) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("RemoveElements(%v, %v) = %v; want %v", tt.allElements, tt.elementsToRemove, result, tt.expected) + } + }) + } +} diff --git a/plugins/caddy/e2e/docker/Caddyfile b/plugins/caddy/e2e/docker/Caddyfile index 4b1340c8..138138ad 100644 --- a/plugins/caddy/e2e/docker/Caddyfile +++ b/plugins/caddy/e2e/docker/Caddyfile @@ -57,4 +57,16 @@ } reverse_proxy nginx:80 } + + route /group { + sablier http://sablier:10000 { + group E2E + session_duration 1m + dynamic { + display_name Group E2E + theme hacker-terminal + } + } + reverse_proxy whoami:80 + } } diff --git a/plugins/caddy/e2e/docker/docker-compose.yml b/plugins/caddy/e2e/docker/docker-compose.yml index 95d668a5..6871a326 100644 --- a/plugins/caddy/e2e/docker/docker-compose.yml +++ b/plugins/caddy/e2e/docker/docker-compose.yml @@ -20,9 +20,15 @@ services: whoami: image: containous/whoami:v1.5.0 + labels: + - sablier.enable=true + - sablier.group=E2E nginx: image: nginx:1.27.0 healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] - interval: 5s \ No newline at end of file + interval: 5s + labels: + - sablier.enable=true + - sablier.group=E2E \ No newline at end of file diff --git a/plugins/caddy/e2e/docker/run.sh b/plugins/caddy/e2e/docker/run.sh index 3c4717c3..0dd0a860 100644 --- a/plugins/caddy/e2e/docker/run.sh +++ b/plugins/caddy/e2e/docker/run.sh @@ -35,5 +35,6 @@ run_docker_classic_test Test_Dynamic run_docker_classic_test Test_Blocking run_docker_classic_test Test_Multiple run_docker_classic_test Test_Healthy +run_docker_classic_test Test_Group exit $errors \ No newline at end of file diff --git a/plugins/caddy/e2e/docker_swarm/Caddyfile b/plugins/caddy/e2e/docker_swarm/Caddyfile index 1075753b..6122d47d 100644 --- a/plugins/caddy/e2e/docker_swarm/Caddyfile +++ b/plugins/caddy/e2e/docker_swarm/Caddyfile @@ -57,4 +57,16 @@ } reverse_proxy nginx:80 } + + route /group { + sablier http://sablier:10000 { + group E2E + session_duration 1m + dynamic { + display_name Group E2E + theme hacker-terminal + } + } + reverse_proxy whoami:80 + } } diff --git a/plugins/caddy/e2e/docker_swarm/docker-stack.yml b/plugins/caddy/e2e/docker_swarm/docker-stack.yml index 5af3b6a8..8919b97d 100644 --- a/plugins/caddy/e2e/docker_swarm/docker-stack.yml +++ b/plugins/caddy/e2e/docker_swarm/docker-stack.yml @@ -26,6 +26,9 @@ services: whoami: image: containous/whoami:v1.5.0 deploy: + labels: + - sablier.enable=true + - sablier.group=E2E replicas: 0 nginx: @@ -34,4 +37,7 @@ services: test: ["CMD", "curl", "-f", "http://localhost"] interval: 5s deploy: + labels: + - sablier.enable=true + - sablier.group=E2E replicas: 0 \ No newline at end of file diff --git a/plugins/caddy/e2e/docker_swarm/run.sh b/plugins/caddy/e2e/docker_swarm/run.sh index d1e7fb89..83c6fce6 100644 --- a/plugins/caddy/e2e/docker_swarm/run.sh +++ b/plugins/caddy/e2e/docker_swarm/run.sh @@ -48,5 +48,6 @@ run_docker_swarm_test Test_Dynamic run_docker_swarm_test Test_Blocking run_docker_swarm_test Test_Multiple run_docker_swarm_test Test_Healthy +run_docker_swarm_test Test_Group exit $errors \ No newline at end of file diff --git a/plugins/caddy/e2e/kubernetes/Caddyfile b/plugins/caddy/e2e/kubernetes/Caddyfile index 32e1b4cf..acf8040d 100644 --- a/plugins/caddy/e2e/kubernetes/Caddyfile +++ b/plugins/caddy/e2e/kubernetes/Caddyfile @@ -19,4 +19,16 @@ sablier url=http://tasks.sablier:10000 names=e2e-nginx-1 session_duration=1m dynamic.display_name=Healthy-Nginx dynamic.theme=hacker-terminal reverse_proxy nginx:80 } + + route /group { + sablier url=http://tasks.sablier:10000 { + group E2E + session_duration 1m + dynamic { + display_name Group E2E + theme hacker-terminal + } + } + reverse_proxy whoami:80 + } } diff --git a/plugins/nginx/e2e/docker/docker-compose.yml b/plugins/nginx/e2e/docker/docker-compose.yml index 5c635d4b..99d62387 100644 --- a/plugins/nginx/e2e/docker/docker-compose.yml +++ b/plugins/nginx/e2e/docker/docker-compose.yml @@ -24,9 +24,15 @@ services: whoami: image: containous/whoami:v1.5.0 + labels: + - sablier.enable=true + - sablier.group=E2E nginx: image: nginx:1.27.0 healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] - interval: 5s \ No newline at end of file + interval: 5s + labels: + - sablier.enable=true + - sablier.group=E2E \ No newline at end of file diff --git a/plugins/nginx/e2e/docker/nginx.conf b/plugins/nginx/e2e/docker/nginx.conf index e7204d69..721469ce 100644 --- a/plugins/nginx/e2e/docker/nginx.conf +++ b/plugins/nginx/e2e/docker/nginx.conf @@ -79,4 +79,14 @@ server { set $sablierDynamicTheme hacker-terminal; js_content sablier.call; } + + location /group { + set $sablierDynamicShowDetails true; + set $sablierDynamicRefreshFrequency 5s; + set $sablierNginxInternalRedirect @whoami; + set $sablierGroup E2E; + set $sablierDynamicName "Group E2E"; + set $sablierDynamicTheme hacker-terminal; + js_content sablier.call; + } } \ No newline at end of file diff --git a/plugins/nginx/e2e/docker/run.sh b/plugins/nginx/e2e/docker/run.sh index 3c4717c3..b3b214bf 100644 --- a/plugins/nginx/e2e/docker/run.sh +++ b/plugins/nginx/e2e/docker/run.sh @@ -10,7 +10,6 @@ docker version prepare_docker_classic() { docker compose -f $DOCKER_COMPOSE_FILE -p $DOCKER_COMPOSE_PROJECT_NAME up -d - docker compose -f $DOCKER_COMPOSE_FILE -p $DOCKER_COMPOSE_PROJECT_NAME stop whoami nginx } destroy_docker_classic() { @@ -35,5 +34,6 @@ run_docker_classic_test Test_Dynamic run_docker_classic_test Test_Blocking run_docker_classic_test Test_Multiple run_docker_classic_test Test_Healthy +run_docker_classic_test Test_Group exit $errors \ No newline at end of file diff --git a/plugins/nginx/e2e/docker_swarm/docker-stack.yml b/plugins/nginx/e2e/docker_swarm/docker-stack.yml index e89ad235..6003d8e1 100644 --- a/plugins/nginx/e2e/docker_swarm/docker-stack.yml +++ b/plugins/nginx/e2e/docker_swarm/docker-stack.yml @@ -28,6 +28,9 @@ services: whoami: image: containous/whoami:v1.5.0 deploy: + labels: + - sablier.enable=true + - sablier.group=E2E replicas: 0 nginx: @@ -36,4 +39,7 @@ services: test: ["CMD", "curl", "-f", "http://localhost"] interval: 5s deploy: + labels: + - sablier.enable=true + - sablier.group=E2E replicas: 0 \ No newline at end of file diff --git a/plugins/nginx/e2e/docker_swarm/nginx.conf b/plugins/nginx/e2e/docker_swarm/nginx.conf index d6800ff5..35555c67 100644 --- a/plugins/nginx/e2e/docker_swarm/nginx.conf +++ b/plugins/nginx/e2e/docker_swarm/nginx.conf @@ -79,4 +79,14 @@ server { set $sablierDynamicTheme hacker-terminal; js_content sablier.call; } + + location /group { + set $sablierDynamicShowDetails true; + set $sablierDynamicRefreshFrequency 5s; + set $sablierNginxInternalRedirect @whoami; + set $sablierGroup E2E; + set $sablierDynamicName "Group E2E"; + set $sablierDynamicTheme hacker-terminal; + js_content sablier.call; + } } \ No newline at end of file diff --git a/plugins/nginx/e2e/docker_swarm/run.sh b/plugins/nginx/e2e/docker_swarm/run.sh index a1010717..b9cc6439 100644 --- a/plugins/nginx/e2e/docker_swarm/run.sh +++ b/plugins/nginx/e2e/docker_swarm/run.sh @@ -47,5 +47,6 @@ run_docker_swarm_test Test_Dynamic run_docker_swarm_test Test_Blocking run_docker_swarm_test Test_Multiple run_docker_swarm_test Test_Healthy +run_docker_swarm_test Test_Group exit $errors \ No newline at end of file diff --git a/plugins/nginx/e2e/kubernetes/manifests/deployment.yml b/plugins/nginx/e2e/kubernetes/manifests/deployment.yml index 6abfbdd7..b7bd13ac 100644 --- a/plugins/nginx/e2e/kubernetes/manifests/deployment.yml +++ b/plugins/nginx/e2e/kubernetes/manifests/deployment.yml @@ -4,6 +4,8 @@ metadata: name: whoami-deployment labels: app: whoami + sablier.enable: true + sablier.group: E2E spec: replicas: 0 selector: @@ -36,6 +38,8 @@ metadata: name: nginx-deployment labels: app: nginx + sablier.enable: true + sablier.group: E2E spec: replicas: 0 selector: @@ -98,4 +102,23 @@ spec: service: name: nginx-service port: - number: 80 \ No newline at end of file + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: group-ingress + annotations: + kubernetes.io/ingress.class: traefik +spec: + rules: + - host: localhost + http: + paths: + - path: /group + pathType: Prefix + backend: + service: + name: nginx-service + port: + number: 80 \ No newline at end of file diff --git a/plugins/nginx/e2e/kubernetes/manifests/sablier.yml b/plugins/nginx/e2e/kubernetes/manifests/sablier.yml index 7b8124ed..98f04bd3 100644 --- a/plugins/nginx/e2e/kubernetes/manifests/sablier.yml +++ b/plugins/nginx/e2e/kubernetes/manifests/sablier.yml @@ -16,7 +16,6 @@ spec: app: sablier spec: serviceAccountName: sablier - serviceAccount: sablier containers: - name: sablier image: acouvreur/sablier:local diff --git a/plugins/proxywasm/e2e/apacheapisix/docker/apisix.yaml b/plugins/proxywasm/e2e/apacheapisix/docker/apisix.yaml index fce78c14..66de123c 100644 --- a/plugins/proxywasm/e2e/apacheapisix/docker/apisix.yaml +++ b/plugins/proxywasm/e2e/apacheapisix/docker/apisix.yaml @@ -43,4 +43,13 @@ routes: type: roundrobin nodes: "nginx:80": 1 + + - uri: "/group" + plugins: + proxywasm_sablier_plugin: + conf: '{ "sablier_url": "sablier:10000", "group": "E2E", "session_duration": "1m", "dynamic": { "display_name": "Group E2E" } }' + upstream: + type: roundrobin + nodes: + "whoami:80": 1 #END \ No newline at end of file diff --git a/plugins/proxywasm/e2e/apacheapisix/docker/compose.yaml b/plugins/proxywasm/e2e/apacheapisix/docker/compose.yaml index ca9be794..bd7cd8d1 100644 --- a/plugins/proxywasm/e2e/apacheapisix/docker/compose.yaml +++ b/plugins/proxywasm/e2e/apacheapisix/docker/compose.yaml @@ -20,9 +20,15 @@ services: whoami: image: containous/whoami:v1.5.0 + labels: + - sablier.enable=true + - sablier.group=E2E nginx: image: nginx:1.27.0 healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] - interval: 5s \ No newline at end of file + interval: 5s + labels: + - sablier.enable=true + - sablier.group=E2E \ No newline at end of file diff --git a/plugins/proxywasm/e2e/apacheapisix/docker/run.sh b/plugins/proxywasm/e2e/apacheapisix/docker/run.sh index f30a2a3d..c51d3bdd 100644 --- a/plugins/proxywasm/e2e/apacheapisix/docker/run.sh +++ b/plugins/proxywasm/e2e/apacheapisix/docker/run.sh @@ -35,5 +35,6 @@ run_docker_classic_test Test_Dynamic run_docker_classic_test Test_Blocking run_docker_classic_test Test_Multiple run_docker_classic_test Test_Healthy +run_docker_classic_test Test_Group exit $errors \ No newline at end of file diff --git a/plugins/proxywasm/e2e/envoy/docker/compose.yaml b/plugins/proxywasm/e2e/envoy/docker/compose.yaml index 5454a5c7..7a42bc8b 100644 --- a/plugins/proxywasm/e2e/envoy/docker/compose.yaml +++ b/plugins/proxywasm/e2e/envoy/docker/compose.yaml @@ -19,9 +19,15 @@ services: whoami: image: containous/whoami:v1.5.0 + labels: + - sablier.enable=true + - sablier.group=E2E nginx: image: nginx:1.27.0 healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] - interval: 5s \ No newline at end of file + interval: 5s + labels: + - sablier.enable=true + - sablier.group=E2E \ No newline at end of file diff --git a/plugins/proxywasm/e2e/envoy/docker/envoy.yaml b/plugins/proxywasm/e2e/envoy/docker/envoy.yaml index 6951fc29..1d455ff3 100644 --- a/plugins/proxywasm/e2e/envoy/docker/envoy.yaml +++ b/plugins/proxywasm/e2e/envoy/docker/envoy.yaml @@ -69,6 +69,16 @@ static_resources: config: # Note this config field could not be empty because the xDS API requirement. "@type": type.googleapis.com/google.protobuf.Empty # Empty as a placeholder. is_optional: true + - match: + path: "/group" + route: + cluster: whoami + typed_per_filter_config: + sablier-wasm-group: + "@type": type.googleapis.com/envoy.config.route.v3.FilterConfig + config: # Note this config field could not be empty because the xDS API requirement. + "@type": type.googleapis.com/google.protobuf.Empty # Empty as a placeholder. + is_optional: true http_filters: - name: sablier-wasm-whoami-dynamic @@ -184,6 +194,34 @@ static_resources: local: filename: "/etc/sablierproxywasm.wasm" configuration: { } + - name: sablier-wasm-group + disabled: true + typed_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + value: + config: + name: "sablier-wasm-group" + root_id: "sablier-wasm-group" + configuration: + "@type": "type.googleapis.com/google.protobuf.StringValue" + value: | + { + "sablier_url": "sablier:10000", + "cluster": "sablier", + "group": "E2E", + "session_duration": "1m", + "dynamic": { + "display_name": "Group E2E" + } + } + vm_config: + runtime: "envoy.wasm.runtime.v8" + vm_id: "vm.sablier.sablier-wasm-group" + code: + local: + filename: "/etc/sablierproxywasm.wasm" + configuration: { } - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/plugins/proxywasm/e2e/envoy/docker/run.sh b/plugins/proxywasm/e2e/envoy/docker/run.sh index c27d646a..fcaaaf6e 100644 --- a/plugins/proxywasm/e2e/envoy/docker/run.sh +++ b/plugins/proxywasm/e2e/envoy/docker/run.sh @@ -35,5 +35,6 @@ run_docker_classic_test Test_Dynamic run_docker_classic_test Test_Blocking run_docker_classic_test Test_Multiple run_docker_classic_test Test_Healthy +run_docker_classic_test Test_Group exit $errors \ No newline at end of file diff --git a/plugins/proxywasm/e2e/nginx/docker/compose.yaml b/plugins/proxywasm/e2e/nginx/docker/compose.yaml index 4dab5470..20abed66 100644 --- a/plugins/proxywasm/e2e/nginx/docker/compose.yaml +++ b/plugins/proxywasm/e2e/nginx/docker/compose.yaml @@ -20,9 +20,15 @@ services: whoami: image: containous/whoami:v1.5.0 + labels: + - sablier.enable=true + - sablier.group=E2E nginx: image: nginx:1.27.0 healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] - interval: 5s \ No newline at end of file + interval: 5s + labels: + - sablier.enable=true + - sablier.group=E2E \ No newline at end of file diff --git a/plugins/proxywasm/e2e/nginx/docker/nginx.conf b/plugins/proxywasm/e2e/nginx/docker/nginx.conf index 672901ff..1ac1d113 100644 --- a/plugins/proxywasm/e2e/nginx/docker/nginx.conf +++ b/plugins/proxywasm/e2e/nginx/docker/nginx.conf @@ -58,5 +58,13 @@ http { proxy_pass http://$proxy_pass_host; proxy_set_header Host localhost:8080; # e2e test compliance } + + location /group { + proxy_wasm proxywasm_sablier_plugin '{ "sablier_url": "sablier:10000", "group": "E2E", "session_duration": "1m", "dynamic": { "display_name": "Group E2E" } }'; + + set $proxy_pass_host whoami:80$request_uri; + proxy_pass http://$proxy_pass_host; + proxy_set_header Host localhost:8080; # e2e test compliance + } } } \ No newline at end of file diff --git a/plugins/proxywasm/e2e/nginx/docker/run.sh b/plugins/proxywasm/e2e/nginx/docker/run.sh index e1e4feae..468409e0 100644 --- a/plugins/proxywasm/e2e/nginx/docker/run.sh +++ b/plugins/proxywasm/e2e/nginx/docker/run.sh @@ -35,5 +35,6 @@ run_docker_classic_test Test_Dynamic run_docker_classic_test Test_Blocking run_docker_classic_test Test_Multiple run_docker_classic_test Test_Healthy +run_docker_classic_test Test_Group exit $errors \ No newline at end of file diff --git a/plugins/traefik/e2e/docker/docker-compose.yml b/plugins/traefik/e2e/docker/docker-compose.yml index 0446f06d..2875c3aa 100644 --- a/plugins/traefik/e2e/docker/docker-compose.yml +++ b/plugins/traefik/e2e/docker/docker-compose.yml @@ -47,12 +47,19 @@ services: - traefik.http.middlewares.healthy.plugin.sablier.sablierUrl=http://sablier:10000 - traefik.http.middlewares.healthy.plugin.sablier.sessionDuration=1m - traefik.http.middlewares.healthy.plugin.sablier.dynamic.displayName=Healthy Nginx + # Group Middleware + - traefik.http.middlewares.group.plugin.sablier.group=E2E + - traefik.http.middlewares.group.plugin.sablier.sablierUrl=http://sablier:10000 + - traefik.http.middlewares.group.plugin.sablier.sessionDuration=1m + - traefik.http.middlewares.group.plugin.sablier.dynamic.displayName=Group E2E whoami: image: containous/whoami:v1.5.0 # Cannot use labels because as soon as the container is stopped, the labels are not treated by Traefik # The route doesn't exist anymore. Use dynamic-config.yml file instead. - # labels: + labels: + - sablier.enable=true + - sablier.group=E2E # - traefik.enable # - traefik.http.routers.whoami.rule=PathPrefix(`/whoami`) # - traefik.http.routers.whoami.middlewares=ondemand @@ -64,7 +71,9 @@ services: interval: 5s # Cannot use labels because as soon as the container is stopped, the labels are not treated by Traefik # The route doesn't exist anymore. Use dynamic-config.yml file instead. - # labels: + labels: + - sablier.enable=true + - sablier.group=E2E # - traefik.enable # - traefik.http.routers.nginx.rule=PathPrefix(`/nginx`) # - traefik.http.routers.nginx.middlewares=ondemand \ No newline at end of file diff --git a/plugins/traefik/e2e/docker/dynamic-config.yml b/plugins/traefik/e2e/docker/dynamic-config.yml index 0f674661..e7643b53 100644 --- a/plugins/traefik/e2e/docker/dynamic-config.yml +++ b/plugins/traefik/e2e/docker/dynamic-config.yml @@ -48,4 +48,12 @@ http: - "http" middlewares: - healthy@docker - service: "nginx" \ No newline at end of file + service: "nginx" + + group: + rule: PathPrefix(`/group`) + entryPoints: + - "http" + middlewares: + - group@docker + service: "whoami" \ No newline at end of file diff --git a/plugins/traefik/e2e/docker/run.sh b/plugins/traefik/e2e/docker/run.sh index 49b23277..44acf618 100644 --- a/plugins/traefik/e2e/docker/run.sh +++ b/plugins/traefik/e2e/docker/run.sh @@ -35,5 +35,6 @@ run_docker_classic_test Test_Dynamic run_docker_classic_test Test_Blocking run_docker_classic_test Test_Multiple run_docker_classic_test Test_Healthy +run_docker_classic_test Test_Group exit $errors \ No newline at end of file diff --git a/plugins/traefik/e2e/docker_swarm/docker-stack.yml b/plugins/traefik/e2e/docker_swarm/docker-stack.yml index c0badf81..7399327d 100644 --- a/plugins/traefik/e2e/docker_swarm/docker-stack.yml +++ b/plugins/traefik/e2e/docker_swarm/docker-stack.yml @@ -2,13 +2,13 @@ version: "3.7" services: traefik: - image: traefik:2.9.1 + image: traefik:v3.0.4 command: - --experimental.localPlugins.sablier.moduleName=github.com/acouvreur/sablier - --entryPoints.http.address=:80 - - --providers.docker=true - - --providers.docker.swarmmode=true - - --providers.docker.swarmModeRefreshSeconds=1 # Default is 15s + - --providers.swarm=true + - --providers.swarm.refreshSeconds=1 # Default is 15s + - --providers.swarm.allowemptyservices=true ports: - target: 80 published: 8080 @@ -54,6 +54,11 @@ services: - traefik.http.middlewares.healthy.plugin.sablier.sablierUrl=http://tasks.sablier:10000 - traefik.http.middlewares.healthy.plugin.sablier.sessionDuration=1m - traefik.http.middlewares.healthy.plugin.sablier.dynamic.displayName=Healthy Nginx + # Group Middleware + - traefik.http.middlewares.group.plugin.sablier.group=E2E + - traefik.http.middlewares.group.plugin.sablier.sablierUrl=http://tasks.sablier:10000 + - traefik.http.middlewares.group.plugin.sablier.sessionDuration=1m + - traefik.http.middlewares.group.plugin.sablier.dynamic.displayName=Group E2E - traefik.http.services.sablier.loadbalancer.server.port=10000 whoami: @@ -61,19 +66,24 @@ services: deploy: replicas: 0 labels: + - sablier.enable=true + - sablier.group=E2E - traefik.enable=true # If you do not use the swarm load balancer, traefik will evict the service from its pool # as soon as the service is 0/0. If you do not set that, fallback to dynamic-config.yml file usage. - traefik.docker.lbswarm=true - - traefik.http.routers.whoami-dynamic.middlewares=dynamic@docker + - traefik.http.routers.whoami-dynamic.middlewares=dynamic@swarm - traefik.http.routers.whoami-dynamic.rule=PathPrefix(`/dynamic/whoami`) - traefik.http.routers.whoami-dynamic.service=whoami - - traefik.http.routers.whoami-blocking.middlewares=blocking@docker + - traefik.http.routers.whoami-blocking.middlewares=blocking@swarm - traefik.http.routers.whoami-blocking.rule=PathPrefix(`/blocking/whoami`) - traefik.http.routers.whoami-blocking.service=whoami - - traefik.http.routers.whoami-multiple.middlewares=multiple@docker + - traefik.http.routers.whoami-multiple.middlewares=multiple@swarm - traefik.http.routers.whoami-multiple.rule=PathPrefix(`/multiple/whoami`) - traefik.http.routers.whoami-multiple.service=whoami + - traefik.http.routers.whoami-group.middlewares=group@swarm + - traefik.http.routers.whoami-group.rule=PathPrefix(`/group`) + - traefik.http.routers.whoami-group.service=whoami - traefik.http.services.whoami.loadbalancer.server.port=80 nginx: @@ -84,14 +94,16 @@ services: deploy: replicas: 0 labels: + - sablier.enable=true + - sablier.group=E2E - traefik.enable=true # If you do not use the swarm load balancer, traefik will evict the service from its pool # as soon as the service is 0/0. If you do not set that, fallback to dynamic-config.yml file usage. - traefik.docker.lbswarm=true - - traefik.http.routers.nginx-multiple.middlewares=multiple@docker + - traefik.http.routers.nginx-multiple.middlewares=multiple@swarm - traefik.http.routers.nginx-multiple.rule=PathPrefix(`/multiple/nginx`) - traefik.http.routers.nginx-multiple.service=nginx - - traefik.http.routers.nginx-healthy.middlewares=healthy@docker + - traefik.http.routers.nginx-healthy.middlewares=healthy@swarm - traefik.http.routers.nginx-healthy.rule=PathPrefix(`/healthy/nginx`) - traefik.http.routers.nginx-healthy.service=nginx - traefik.http.services.nginx.loadbalancer.server.port=80 diff --git a/plugins/traefik/e2e/docker_swarm/run.sh b/plugins/traefik/e2e/docker_swarm/run.sh index dc30fe8e..6d67d94a 100644 --- a/plugins/traefik/e2e/docker_swarm/run.sh +++ b/plugins/traefik/e2e/docker_swarm/run.sh @@ -47,5 +47,6 @@ run_docker_swarm_test Test_Dynamic run_docker_swarm_test Test_Blocking run_docker_swarm_test Test_Multiple run_docker_swarm_test Test_Healthy +run_docker_swarm_test Test_Group exit $errors \ No newline at end of file diff --git a/plugins/traefik/e2e/kubernetes/docker-kubernetes.yml b/plugins/traefik/e2e/kubernetes/docker-kubernetes.yml index fad9332d..6f635f62 100644 --- a/plugins/traefik/e2e/kubernetes/docker-kubernetes.yml +++ b/plugins/traefik/e2e/kubernetes/docker-kubernetes.yml @@ -1,8 +1,8 @@ version: '3' services: server: - image: "rancher/k3s:v1.23.12-k3s1" - command: server --no-deploy traefik + image: "rancher/k3s:v1.30.2-k3s1" + command: server --disable=traefik tmpfs: - /run - /var/run diff --git a/plugins/traefik/e2e/kubernetes/manifests/deployment.yml b/plugins/traefik/e2e/kubernetes/manifests/deployment.yml index 8a89a493..a2375377 100644 --- a/plugins/traefik/e2e/kubernetes/manifests/deployment.yml +++ b/plugins/traefik/e2e/kubernetes/manifests/deployment.yml @@ -4,6 +4,8 @@ metadata: name: whoami-deployment labels: app: whoami + sablier.enable: "true" + sablier.group: "E2E" spec: replicas: 0 selector: @@ -30,7 +32,7 @@ spec: selector: app: whoami --- -apiVersion: traefik.containo.us/v1alpha1 +apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: dynamic @@ -44,7 +46,7 @@ spec: dynamic: displayName: 'Dynamic Whoami' --- -apiVersion: traefik.containo.us/v1alpha1 +apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: blocking @@ -59,7 +61,7 @@ spec: timeout: 30s --- -apiVersion: traefik.containo.us/v1alpha1 +apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: multiple @@ -73,12 +75,26 @@ spec: dynamic: displayName: 'Multiple Whoami' --- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: group + namespace: default +spec: + plugin: + sablier: + group: E2E + sablierUrl: 'http://sablier:10000' + sessionDuration: 1m + dynamic: + displayName: 'Group E2E' +--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: whoami-dynamic-ingress annotations: - kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.entrypoints: web traefik.ingress.kubernetes.io/router.middlewares: default-dynamic@kubernetescrd spec: rules: @@ -98,7 +114,7 @@ kind: Ingress metadata: name: whoami-blocking-ingress annotations: - kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.entrypoints: web traefik.ingress.kubernetes.io/router.middlewares: default-blocking@kubernetescrd spec: rules: @@ -118,7 +134,7 @@ kind: Ingress metadata: name: whoami-multiple-ingress annotations: - kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.entrypoints: web traefik.ingress.kubernetes.io/router.middlewares: default-multiple@kubernetescrd spec: rules: @@ -133,13 +149,14 @@ spec: port: number: 80 --- ---- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx + sablier.enable: "true" + sablier.group: "E2E" spec: replicas: 0 selector: @@ -166,7 +183,7 @@ spec: selector: app: nginx --- -apiVersion: traefik.containo.us/v1alpha1 +apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: healthy @@ -185,7 +202,7 @@ kind: Ingress metadata: name: nginx-multiple-ingress annotations: - kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.entrypoints: web traefik.ingress.kubernetes.io/router.middlewares: default-multiple@kubernetescrd spec: rules: @@ -205,7 +222,7 @@ kind: Ingress metadata: name: nginx-healthy-ingress annotations: - kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.entrypoints: web traefik.ingress.kubernetes.io/router.middlewares: default-healthy@kubernetescrd spec: rules: @@ -218,4 +235,24 @@ spec: service: name: nginx-service port: - number: 80 \ No newline at end of file + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: group-ingress + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + traefik.ingress.kubernetes.io/router.middlewares: default-group@kubernetescrd +spec: + rules: + - host: localhost + http: + paths: + - path: /group + pathType: Prefix + backend: + service: + name: whoami-service + port: + number: 80 \ No newline at end of file diff --git a/plugins/traefik/e2e/kubernetes/manifests/sablier.yml b/plugins/traefik/e2e/kubernetes/manifests/sablier.yml index 7b8124ed..98f04bd3 100644 --- a/plugins/traefik/e2e/kubernetes/manifests/sablier.yml +++ b/plugins/traefik/e2e/kubernetes/manifests/sablier.yml @@ -16,7 +16,6 @@ spec: app: sablier spec: serviceAccountName: sablier - serviceAccount: sablier containers: - name: sablier image: acouvreur/sablier:local diff --git a/plugins/traefik/e2e/kubernetes/run.sh b/plugins/traefik/e2e/kubernetes/run.sh index 57009f21..2ee733a9 100644 --- a/plugins/traefik/e2e/kubernetes/run.sh +++ b/plugins/traefik/e2e/kubernetes/run.sh @@ -25,7 +25,7 @@ destroy_kubernetes() { prepare_traefik() { helm repo add traefik https://traefik.github.io/charts helm repo update - helm install traefik --version 27.0.2 traefik/traefik -f values.yaml --namespace kube-system + helm install traefik --version 28.3.0 traefik/traefik -f values.yaml --namespace kube-system } prepare_deployment() { @@ -68,5 +68,6 @@ run_kubernetes_deployment_test Test_Dynamic run_kubernetes_deployment_test Test_Blocking run_kubernetes_deployment_test Test_Multiple run_kubernetes_deployment_test Test_Healthy +run_kubernetes_deployment_test Test_Group exit $errors