mirror of
https://github.com/sablierapp/sablier.git
synced 2026-01-03 11:34:58 +01:00
info takes health into account
This commit is contained in:
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/sablierapp/sablier/pkg/provider"
|
||||
"github.com/sablierapp/sablier/pkg/sablier"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ sablier.Provider = (*DockerProvider)(nil)
|
||||
@@ -62,16 +61,6 @@ func (d *DockerProvider) Stop(ctx context.Context, name string) error {
|
||||
return d.Client.ContainerStop(ctx, name, container.StopOptions{})
|
||||
}
|
||||
|
||||
func (d *DockerProvider) Info(ctx context.Context, name string) (sablier.InstanceInfo, error) {
|
||||
return sablier.InstanceInfo{
|
||||
Name: name,
|
||||
CurrentReplicas: 0,
|
||||
DesiredReplicas: 0,
|
||||
Status: sablier.InstanceStarting,
|
||||
StartedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DockerProvider) List(ctx context.Context, opts provider.ListOptions) ([]sablier.InstanceConfig, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
|
||||
@@ -11,10 +11,13 @@ import (
|
||||
|
||||
func (d *DockerProvider) AfterReady(ctx context.Context, name string) <-chan error {
|
||||
ch := make(chan error, 1)
|
||||
started := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c, err := d.Client.ContainerInspect(ctx, name)
|
||||
if err != nil {
|
||||
close(started)
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
@@ -30,6 +33,7 @@ func (d *DockerProvider) AfterReady(ctx context.Context, name string) <-chan err
|
||||
ready := d.afterAction(ctx, name, action)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
||||
close(started)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -51,23 +55,27 @@ func (d *DockerProvider) AfterReady(ctx context.Context, name string) <-chan err
|
||||
}
|
||||
}
|
||||
}()
|
||||
<-started
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (d *DockerProvider) afterAction(ctx context.Context, name string, action events.Action) <-chan error {
|
||||
ch := make(chan error, 1)
|
||||
|
||||
msgs, errs := d.Client.Events(ctx, events.ListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("scope", "local"),
|
||||
filters.Arg("type", string(events.ContainerEventType)),
|
||||
filters.Arg("container", name),
|
||||
filters.Arg("event", string(action)),
|
||||
),
|
||||
})
|
||||
started := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
msgs, errs := d.Client.Events(ctx, events.ListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("scope", "local"),
|
||||
filters.Arg("type", string(events.ContainerEventType)),
|
||||
filters.Arg("container", name),
|
||||
filters.Arg("event", string(action)),
|
||||
),
|
||||
})
|
||||
|
||||
close(started)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -90,6 +98,7 @@ func (d *DockerProvider) afterAction(ctx context.Context, name string, action ev
|
||||
}
|
||||
}
|
||||
}()
|
||||
<-started
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
8
pkg/provider/docker/format.go
Normal file
8
pkg/provider/docker/format.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package docker
|
||||
|
||||
import "strings"
|
||||
|
||||
// FormatName removes the container name `/` prefix returned by the Docker API
|
||||
func FormatName(name string) string {
|
||||
return strings.TrimPrefix(name, "/")
|
||||
}
|
||||
53
pkg/provider/docker/info.go
Normal file
53
pkg/provider/docker/info.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/sablierapp/sablier/pkg/sablier"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (d *DockerProvider) Info(ctx context.Context, name string) (sablier.InstanceInfo, error) {
|
||||
spec, err := d.Client.ContainerInspect(ctx, name)
|
||||
if err != nil {
|
||||
return sablier.InstanceInfo{}, err
|
||||
}
|
||||
|
||||
// String representation of the container state.
|
||||
// Can be one of "created", "running", "paused", "restarting", "removing", "exited", or "dead"
|
||||
switch spec.State.Status {
|
||||
case "created", "paused", "exited", "dead":
|
||||
return sablier.InstanceInfo{
|
||||
Name: FormatName(spec.Name),
|
||||
CurrentReplicas: 0,
|
||||
DesiredReplicas: 1,
|
||||
Status: sablier.InstanceDown,
|
||||
StartedAt: time.Time{},
|
||||
}, nil
|
||||
case "running":
|
||||
startedAt, err := time.Parse(time.RFC3339Nano, spec.State.StartedAt)
|
||||
if err != nil {
|
||||
return sablier.InstanceInfo{}, err
|
||||
}
|
||||
|
||||
if spec.State.Health != nil && spec.State.Health.Status != "healthy" {
|
||||
return sablier.InstanceInfo{
|
||||
Name: FormatName(spec.Name),
|
||||
CurrentReplicas: 0,
|
||||
DesiredReplicas: 1,
|
||||
Status: sablier.InstanceStarting,
|
||||
StartedAt: startedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return sablier.InstanceInfo{
|
||||
Name: FormatName(spec.Name),
|
||||
CurrentReplicas: 1,
|
||||
DesiredReplicas: 1,
|
||||
Status: sablier.InstanceReady,
|
||||
StartedAt: startedAt,
|
||||
}, nil
|
||||
default:
|
||||
return sablier.InstanceInfo{}, fmt.Errorf("unknown container status: %s", spec.State.Status)
|
||||
}
|
||||
}
|
||||
151
pkg/provider/docker/info_test.go
Normal file
151
pkg/provider/docker/info_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package docker_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/sablierapp/sablier/pkg/provider/docker"
|
||||
"github.com/sablierapp/sablier/pkg/sablier"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDockerProvider_Info(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type args struct {
|
||||
do func(dind *dindContainer) error
|
||||
name string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want sablier.InstanceInfo
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "container is created",
|
||||
args: args{
|
||||
do: func(dind *dindContainer) error {
|
||||
_, err := dind.CreateMimic(ctx, MimicOptions{
|
||||
Name: "test-info-created",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
name: "test-info-created",
|
||||
},
|
||||
want: sablier.InstanceInfo{
|
||||
Name: "test-info-created",
|
||||
CurrentReplicas: 0,
|
||||
DesiredReplicas: 1,
|
||||
Status: sablier.InstanceDown,
|
||||
StartedAt: time.Time{},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "container is paused",
|
||||
args: args{
|
||||
do: func(dind *dindContainer) error {
|
||||
mimic, err := dind.CreateMimic(ctx, MimicOptions{
|
||||
Name: "test-info-paused",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = dind.client.ContainerStart(ctx, mimic.ID, container.StartOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dind.client.ContainerPause(ctx, mimic.ID)
|
||||
},
|
||||
name: "test-info-paused",
|
||||
},
|
||||
want: sablier.InstanceInfo{
|
||||
Name: "test-info-paused",
|
||||
CurrentReplicas: 0,
|
||||
DesiredReplicas: 1,
|
||||
Status: sablier.InstanceDown,
|
||||
StartedAt: time.Time{},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "container is exited",
|
||||
args: args{
|
||||
do: func(dind *dindContainer) error {
|
||||
mimic, err := dind.CreateMimic(ctx, MimicOptions{
|
||||
Name: "test-info-exited",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = dind.client.ContainerStart(ctx, mimic.ID, container.StartOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dind.client.ContainerStop(ctx, mimic.ID, container.StopOptions{})
|
||||
},
|
||||
name: "test-info-exited",
|
||||
},
|
||||
want: sablier.InstanceInfo{
|
||||
Name: "test-info-exited",
|
||||
CurrentReplicas: 0,
|
||||
DesiredReplicas: 1,
|
||||
Status: sablier.InstanceDown,
|
||||
StartedAt: time.Time{},
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "container is running (no healthcheck)",
|
||||
args: args{
|
||||
do: func(dind *dindContainer) error {
|
||||
mimic, err := dind.CreateMimic(ctx, MimicOptions{
|
||||
Name: "test-info-running-no-healthcheck",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dind.client.ContainerStart(ctx, mimic.ID, container.StartOptions{})
|
||||
},
|
||||
name: "test-info-running-no-healthcheck",
|
||||
},
|
||||
want: sablier.InstanceInfo{
|
||||
Name: "test-info-running-no-healthcheck",
|
||||
CurrentReplicas: 1,
|
||||
DesiredReplicas: 1,
|
||||
Status: sablier.InstanceReady,
|
||||
StartedAt: time.Now(),
|
||||
},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dinD, err := setupDinD(t, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := docker.NewDockerProvider(dinD.client)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = tt.args.do(dinD)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := d.Info(ctx, tt.args.name)
|
||||
if !tt.wantErr(t, err, fmt.Sprintf("Info(ctx, %v)", tt.args.name)) {
|
||||
return
|
||||
}
|
||||
assert.NotNil(t, got.StartedAt)
|
||||
// assert.NotEqual(t, time.Time{}, got.StartedAt) // When instance is not started, this is not set
|
||||
got.StartedAt = tt.want.StartedAt // Cannot assert equal on that field otherwise
|
||||
assert.Equalf(t, tt.want, got, "Info(ctx, %v)", tt.args.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,113 +4,16 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sablierapp/sablier/pkg/provider"
|
||||
"github.com/sablierapp/sablier/pkg/provider/docker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type dindContainer struct {
|
||||
testcontainers.Container
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
type MimicOptions struct {
|
||||
WithHealth bool
|
||||
HealthyAfter time.Duration
|
||||
RunningAfter time.Duration
|
||||
SablierGroup string
|
||||
}
|
||||
|
||||
func (d *dindContainer) CreateMimic(ctx context.Context, opts MimicOptions) (container.CreateResponse, error) {
|
||||
i, err := d.client.ImagePull(ctx, "docker.io/sablierapp/mimic:v0.3.1", image.PullOptions{})
|
||||
if err != nil {
|
||||
return container.CreateResponse{}, err
|
||||
}
|
||||
_, err = d.client.ImageLoad(ctx, i, false)
|
||||
if err != nil {
|
||||
return container.CreateResponse{}, err
|
||||
}
|
||||
|
||||
if opts.WithHealth == false {
|
||||
return d.client.ContainerCreate(ctx, &container.Config{
|
||||
Cmd: []string{"/mimic", "-running", "-running-after", opts.RunningAfter.String(), "-healthy=false"},
|
||||
Image: "docker.io/sablierapp/mimic:v0.3.1",
|
||||
Labels: map[string]string{
|
||||
"sablier.enable": "true",
|
||||
"sablier.group": opts.SablierGroup,
|
||||
},
|
||||
}, nil, nil, nil, "")
|
||||
}
|
||||
return d.client.ContainerCreate(ctx, &container.Config{
|
||||
Cmd: []string{"/mimic", "-running", "-running-after", opts.RunningAfter.String(), "-healthy", "--healthy-after", opts.HealthyAfter.String()},
|
||||
Healthcheck: &container.HealthConfig{
|
||||
Test: []string{"CMD", "/mimic", "healthcheck"},
|
||||
Interval: time.Second,
|
||||
Timeout: 3 * time.Second,
|
||||
StartPeriod: opts.RunningAfter,
|
||||
StartInterval: time.Second,
|
||||
Retries: 15,
|
||||
},
|
||||
Image: "docker.io/sablierapp/mimic:v0.3.1",
|
||||
Labels: map[string]string{
|
||||
"sablier.enable": "true",
|
||||
"sablier.group": opts.SablierGroup,
|
||||
},
|
||||
}, nil, nil, nil, "")
|
||||
}
|
||||
|
||||
func setupDinD(t *testing.T, ctx context.Context) (*dindContainer, error) {
|
||||
req := testcontainers.ContainerRequest{
|
||||
Image: "docker:dind",
|
||||
ExposedPorts: []string{"2375/tcp"},
|
||||
WaitingFor: wait.ForLog("API listen on [::]:2375"),
|
||||
Cmd: []string{
|
||||
"dockerd", "-H", "tcp://0.0.0.0:2375", "--tls=false",
|
||||
},
|
||||
Privileged: true,
|
||||
}
|
||||
c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: req,
|
||||
Started: true,
|
||||
Logger: testcontainers.TestLogger(t),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ip, err := c.Host(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mappedPort, err := c.MappedPort(ctx, "2375")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// DOCKER_HOST
|
||||
host := fmt.Sprintf("http://%s:%s", ip, mappedPort.Port())
|
||||
fmt.Println("DOCKER_HOST: ", host)
|
||||
cli, err := client.NewClientWithOpts(client.WithHost(host), client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dindContainer{
|
||||
Container: c,
|
||||
client: cli,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestDockerProvider_StartWithHealthcheck(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
dind, err := setupDinD(t, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -155,7 +58,8 @@ func TestDockerProvider_StartWithHealthcheck(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDockerProvider_StartWithoutHealthcheck(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
dind, err := setupDinD(t, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -196,3 +100,24 @@ func TestDockerProvider_StartWithoutHealthcheck(t *testing.T) {
|
||||
t.Logf("inspect: %+v\n", string(resp))
|
||||
assert.Equal(t, inspect.State.Status, "running")
|
||||
}
|
||||
|
||||
func TestDockerProvider_StartNonExistingContainer(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
dind, err := setupDinD(t, ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p, err := docker.NewDockerProvider(dind.client)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = p.Start(ctx, "non-existent", provider.StartOptions{
|
||||
DesiredReplicas: 1,
|
||||
ConsiderReadyAfter: 0,
|
||||
})
|
||||
fmt.Printf("error: %v", err)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
107
pkg/provider/docker/testcontainers_test.go
Normal file
107
pkg/provider/docker/testcontainers_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package docker_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type dindContainer struct {
|
||||
testcontainers.Container
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
type MimicOptions struct {
|
||||
Name string
|
||||
WithHealth bool
|
||||
HealthyAfter time.Duration
|
||||
RunningAfter time.Duration
|
||||
SablierGroup string
|
||||
}
|
||||
|
||||
func (d *dindContainer) CreateMimic(ctx context.Context, opts MimicOptions) (container.CreateResponse, error) {
|
||||
i, err := d.client.ImagePull(ctx, "docker.io/sablierapp/mimic:v0.3.1", image.PullOptions{})
|
||||
if err != nil {
|
||||
return container.CreateResponse{}, err
|
||||
}
|
||||
_, err = d.client.ImageLoad(ctx, i, false)
|
||||
if err != nil {
|
||||
return container.CreateResponse{}, err
|
||||
}
|
||||
|
||||
if opts.WithHealth == false {
|
||||
return d.client.ContainerCreate(ctx, &container.Config{
|
||||
Cmd: []string{"/mimic", "-running", "-running-after", opts.RunningAfter.String(), "-healthy=false"},
|
||||
Image: "docker.io/sablierapp/mimic:v0.3.1",
|
||||
Labels: map[string]string{
|
||||
"sablier.enable": "true",
|
||||
"sablier.group": opts.SablierGroup,
|
||||
},
|
||||
}, nil, nil, nil, opts.Name)
|
||||
}
|
||||
return d.client.ContainerCreate(ctx, &container.Config{
|
||||
Cmd: []string{"/mimic", "-running", "-running-after", opts.RunningAfter.String(), "-healthy", "--healthy-after", opts.HealthyAfter.String()},
|
||||
Healthcheck: &container.HealthConfig{
|
||||
Test: []string{"CMD", "/mimic", "healthcheck"},
|
||||
Interval: 100 * time.Millisecond,
|
||||
Timeout: time.Second,
|
||||
StartPeriod: opts.RunningAfter,
|
||||
StartInterval: time.Second,
|
||||
Retries: 50,
|
||||
},
|
||||
Image: "docker.io/sablierapp/mimic:v0.3.1",
|
||||
Labels: map[string]string{
|
||||
"sablier.enable": "true",
|
||||
"sablier.group": opts.SablierGroup,
|
||||
},
|
||||
}, nil, nil, nil, opts.Name)
|
||||
}
|
||||
|
||||
func setupDinD(t *testing.T, ctx context.Context) (*dindContainer, error) {
|
||||
req := testcontainers.ContainerRequest{
|
||||
Image: "docker:dind",
|
||||
ExposedPorts: []string{"2375/tcp"},
|
||||
WaitingFor: wait.ForLog("API listen on [::]:2375"),
|
||||
Cmd: []string{
|
||||
"dockerd", "-H", "tcp://0.0.0.0:2375", "--tls=false",
|
||||
},
|
||||
Privileged: true,
|
||||
}
|
||||
c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: req,
|
||||
Started: true,
|
||||
Logger: testcontainers.TestLogger(t),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ip, err := c.Host(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mappedPort, err := c.MappedPort(ctx, "2375")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// DOCKER_HOST
|
||||
host := fmt.Sprintf("http://%s:%s", ip, mappedPort.Port())
|
||||
fmt.Println("DOCKER_HOST: ", host)
|
||||
cli, err := client.NewClientWithOpts(client.WithHost(host), client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dindContainer{
|
||||
Container: c,
|
||||
client: cli,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user