[];
// Type $$Props to narrow the handler function based on wether this is an update or a new app
@@ -167,19 +172,35 @@
production |
- {prodScheme}{appName}.{prodUrl}
+
|
- {prodScheme}dashboard.{appName}.{prodUrl}
+
|
staging |
- {stagingScheme}{appName}-staging.{stagingUrl}
+
|
- {stagingScheme}dashboard.{appName}-staging.{stagingUrl}
+
|
@@ -223,7 +244,7 @@
{@html l.translate('app.environment.target.changed.description', [
- initialData.production.target.url
+ initialData.production.target.name
])}
@@ -252,7 +273,7 @@
{@html l.translate('app.environment.target.changed.description', [
- initialData.production.target.url
+ initialData.production.target.name
])}
diff --git a/cmd/serve/front/src/routes/(main)/apps/service-url.svelte b/cmd/serve/front/src/routes/(main)/apps/service-url.svelte
new file mode 100644
index 00000000..96419b33
--- /dev/null
+++ b/cmd/serve/front/src/routes/(main)/apps/service-url.svelte
@@ -0,0 +1,15 @@
+
+
+{#if scheme}
+ {scheme}{prefix}{appName}{suffix}.{host}
+{:else}
+ - ({l.translate('target.manual_proxy')})
+{/if}
diff --git a/cmd/serve/front/src/routes/(main)/targets/[id]/edit/+page.svelte b/cmd/serve/front/src/routes/(main)/targets/[id]/edit/+page.svelte
index 844e8a06..8cae3108 100644
--- a/cmd/serve/front/src/routes/(main)/targets/[id]/edit/+page.svelte
+++ b/cmd/serve/front/src/routes/(main)/targets/[id]/edit/+page.svelte
@@ -14,7 +14,7 @@
export let data;
const submit = (d: UpdateTarget) =>
- service.update(data.target.id, d).then((t) => goto(routes.targets));
+ service.update(data.target.id, d).then(() => goto(routes.targets));
const {
loading: deleting,
diff --git a/cmd/serve/front/src/routes/(main)/targets/target-form.svelte b/cmd/serve/front/src/routes/(main)/targets/target-form.svelte
index b12201ba..bc9b4855 100644
--- a/cmd/serve/front/src/routes/(main)/targets/target-form.svelte
+++ b/cmd/serve/front/src/routes/(main)/targets/target-form.svelte
@@ -26,7 +26,8 @@
let name = initialData?.name ?? '';
let url = initialData?.url ?? '';
let provider: ProviderTypes = initialData?.provider.kind ?? providerTypes[0];
- let isRemote = !!initialData?.provider.data.host ?? false;
+ let isRemote = !!initialData?.provider.data.host || false;
+ let automaticProxyConfiguration = initialData ? !!initialData.url : true;
const docker = { ...initialData?.provider.data };
@@ -48,7 +49,7 @@
if (!initialData) {
formData = {
name,
- url,
+ url: automaticProxyConfiguration ? url : undefined,
docker:
provider === 'docker'
? isRemote
@@ -64,7 +65,7 @@
} else {
formData = {
name,
- url,
+ url: automaticProxyConfiguration ? (initialData?.url !== url ? url : undefined) : null,
docker:
provider === 'docker'
? isRemote
@@ -114,9 +115,17 @@
{l.translate('target.name.help')}
-
- {@html l.translate('target.url.help')}
-
+
+ {@html l.translate('target.automatic_proxy_configuration.help')}
+
+ {#if automaticProxyConfiguration}
+
+ {@html l.translate('target.url.help')}
+
+ {/if}
diff --git a/docs/reference/providers/docker.md b/docs/reference/providers/docker.md
index 6a51513c..6f1795d4 100644
--- a/docs/reference/providers/docker.md
+++ b/docs/reference/providers/docker.md
@@ -35,7 +35,7 @@ Where `ENVIRONMENT` will be one of `production`, `staging`.
## Exposing services
-Once a valid compose file has been found, **seelf** will apply some **heuristics** to determine which services should be exposed and where.
+Once a valid compose file has been found and **only if** the target [manages the proxy itself](/reference/targets#proxy), **seelf** will apply some **heuristics** to determine which services should be exposed and where.
It will consider any service with **port mappings** to be exposed.
diff --git a/docs/reference/targets.md b/docs/reference/targets.md
index 5dedd74a..9d24c58c 100644
--- a/docs/reference/targets.md
+++ b/docs/reference/targets.md
@@ -6,9 +6,16 @@ Targets represents an **host** where your deployments will be exposed. When conf
For now, only one target per host is allowed.
:::
-## Url
+## Proxy configuration {#proxy}
-The url **determine where your applications will be made available**. It should be a **root url** as applications will use subdomains on it.
+When declaring a target, you must choose how the proxy (needed to make your services available from the outside world) should be managed:
+
+- **Automatic**: **seelf** will deploy and configure a [traefik](https://traefik.io/traefik/) proxy on the target. Services urls will be automatically generated based on the [target's url](#url) and [service file](/reference/providers/docker#exposing-services) when deploying. Exposed services will also join the proxy network.
+- **Manual**: you're in charge of **everything** related to services exposure. **seelf** will deploy services on this target without attempting to expose them in any way.
+
+### Url
+
+If the target manages the proxy itself, this url **determines where your applications will be made available**. It should be a **root url** as applications will use subdomains on it.
The scheme associated with this url (`http` or `https`) will determine if certificates should be generated or not.
diff --git a/internal/deployment/app/create_target/create_target.go b/internal/deployment/app/create_target/create_target.go
index cbf67faf..7636348f 100644
--- a/internal/deployment/app/create_target/create_target.go
+++ b/internal/deployment/app/create_target/create_target.go
@@ -6,6 +6,7 @@ import (
auth "github.com/YuukanOO/seelf/internal/auth/domain"
"github.com/YuukanOO/seelf/internal/deployment/domain"
"github.com/YuukanOO/seelf/pkg/bus"
+ "github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/validate"
"github.com/YuukanOO/seelf/pkg/validate/strings"
)
@@ -13,9 +14,9 @@ import (
type Command struct {
bus.Command[string]
- Name string `json:"name"`
- Url string `json:"url"`
- Provider any `json:"-"`
+ Name string `json:"name"`
+ Url monad.Maybe[string] `json:"url"`
+ Provider any `json:"-"`
}
func (Command) Name_() string { return "deployment.command.create_target" }
@@ -30,7 +31,9 @@ func Handler(
if err := validate.Struct(validate.Of{
"name": validate.Field(cmd.Name, strings.Required),
- "url": validate.Value(cmd.Url, &targetUrl, domain.UrlFrom),
+ "url": validate.Maybe(cmd.Url, func(url string) error {
+ return validate.Value(url, &targetUrl, domain.UrlFrom)
+ }),
}); err != nil {
return "", err
}
@@ -42,10 +45,14 @@ func Handler(
}
// Validate availability of both the target domain and the config
- urlRequirement, err := reader.CheckUrlAvailability(ctx, targetUrl)
+ var urlRequirement domain.TargetUrlRequirement
- if err != nil {
- return "", err
+ if cmd.Url.HasValue() {
+ urlRequirement, err = reader.CheckUrlAvailability(ctx, targetUrl)
+
+ if err != nil {
+ return "", err
+ }
}
configRequirement, err := reader.CheckConfigAvailability(ctx, config)
@@ -55,7 +62,7 @@ func Handler(
}
if err = validate.Struct(validate.Of{
- "url": urlRequirement.Error(),
+ "url": validate.If(cmd.Url.HasValue(), urlRequirement.Error),
config.Kind(): configRequirement.Error(),
}); err != nil {
return "", err
@@ -63,7 +70,6 @@ func Handler(
target, err := domain.NewTarget(
cmd.Name,
- urlRequirement,
configRequirement,
auth.CurrentUser(ctx).MustGet(),
)
@@ -72,6 +78,12 @@ func Handler(
return "", err
}
+ if cmd.Url.HasValue() {
+ if err = target.ExposeServicesAutomatically(urlRequirement); err != nil {
+ return "", err
+ }
+ }
+
if err = writer.Write(ctx, &target); err != nil {
return "", err
}
diff --git a/internal/deployment/app/create_target/create_target_test.go b/internal/deployment/app/create_target/create_target_test.go
index 20d2f29a..02ce76fe 100644
--- a/internal/deployment/app/create_target/create_target_test.go
+++ b/internal/deployment/app/create_target/create_target_test.go
@@ -12,6 +12,7 @@ import (
"github.com/YuukanOO/seelf/pkg/bus"
"github.com/YuukanOO/seelf/pkg/bus/spy"
shared "github.com/YuukanOO/seelf/pkg/domain"
+ "github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/must"
"github.com/YuukanOO/seelf/pkg/validate"
"github.com/YuukanOO/seelf/pkg/validate/strings"
@@ -35,7 +36,6 @@ func Test_CreateTarget(t *testing.T) {
assert.ValidationError(t, validate.FieldErrors{
"name": strings.ErrRequired,
- "url": domain.ErrInvalidUrl,
}, err)
})
@@ -45,14 +45,14 @@ func Test_CreateTarget(t *testing.T) {
target := fixture.Target(
fixture.WithTargetCreatedBy(user.ID()),
fixture.WithProviderConfig(config),
- fixture.WithTargetUrl(must.Panic(domain.UrlFrom("http://example.com"))),
)
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true)))
handler, ctx, _ := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target))
_, err := handler(ctx, create_target.Command{
Name: "target",
- Url: "http://example.com",
+ Url: monad.Value("http://example.com"),
Provider: config,
})
@@ -67,12 +67,35 @@ func Test_CreateTarget(t *testing.T) {
_, err := handler(ctx, create_target.Command{
Name: "target",
- Url: "http://example.com",
})
assert.ErrorIs(t, domain.ErrNoValidProviderFound, err)
})
+ t.Run("should allow multiple manual targets to co-exists", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(fixture.WithTargetCreatedBy(user.ID()))
+ handler, ctx, dispatcher := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target))
+
+ id, err := handler(ctx, create_target.Command{
+ Name: "target-one",
+ Provider: fixture.ProviderConfig(),
+ })
+
+ assert.Nil(t, err)
+ assert.NotZero(t, id)
+
+ id, err = handler(ctx, create_target.Command{
+ Name: "target-two",
+ Provider: fixture.ProviderConfig(),
+ })
+
+ assert.Nil(t, err)
+ assert.NotZero(t, id)
+
+ assert.HasLength(t, 2, dispatcher.Signals())
+ })
+
t.Run("should create a new target", func(t *testing.T) {
var config = fixture.ProviderConfig()
user := authfixture.User()
@@ -80,24 +103,31 @@ func Test_CreateTarget(t *testing.T) {
id, err := handler(ctx, create_target.Command{
Name: "target",
- Url: "http://example.com",
+ Url: monad.Value("http://example.com"),
Provider: config,
})
assert.Nil(t, err)
assert.NotZero(t, id)
- assert.HasLength(t, 1, dispatcher.Signals())
+ assert.HasLength(t, 3, dispatcher.Signals())
created := assert.Is[domain.TargetCreated](t, dispatcher.Signals()[0])
assert.DeepEqual(t, domain.TargetCreated{
ID: domain.TargetID(id),
Name: "target",
- Url: must.Panic(domain.UrlFrom("http://example.com")),
State: created.State,
Entrypoints: make(domain.TargetEntrypoints),
Provider: config, // Since the mock returns the config "as is"
Created: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Created.At())),
}, created)
+
+ urlChanged := assert.Is[domain.TargetUrlChanged](t, dispatcher.Signals()[1])
+ assert.Equal(t, domain.TargetUrlChanged{
+ ID: domain.TargetID(id),
+ Url: must.Panic(domain.UrlFrom("http://example.com")),
+ }, urlChanged)
+
+ assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[2])
})
}
diff --git a/internal/deployment/app/expose_seelf_container/expose_seelf_container.go b/internal/deployment/app/expose_seelf_container/expose_seelf_container.go
index 004bd130..f8658ea3 100644
--- a/internal/deployment/app/expose_seelf_container/expose_seelf_container.go
+++ b/internal/deployment/app/expose_seelf_container/expose_seelf_container.go
@@ -78,7 +78,6 @@ func Handler(
}
target, err = domain.NewTarget("local",
- urlRequirement,
configRequirement,
auth.CurrentUser(ctx).MustGet(),
)
@@ -87,6 +86,10 @@ func Handler(
return bus.Unit, err
}
+ if err = target.ExposeServicesAutomatically(urlRequirement); err != nil {
+ return bus.Unit, err
+ }
+
assigned, err := provider.Setup(ctx, target)
target.Configured(target.CurrentVersion(), assigned, err)
diff --git a/internal/deployment/app/get_target/get_target.go b/internal/deployment/app/get_target/get_target.go
index 542b7775..74a50379 100644
--- a/internal/deployment/app/get_target/get_target.go
+++ b/internal/deployment/app/get_target/get_target.go
@@ -22,7 +22,7 @@ type (
Target struct {
ID string `json:"id"`
Name string `json:"name"`
- Url string `json:"url"`
+ Url monad.Maybe[string] `json:"url"`
Provider Provider `json:"provider"`
State State `json:"state"`
CleanupRequestedAt monad.Maybe[time.Time] `json:"cleanup_requested_at"`
diff --git a/internal/deployment/app/query.go b/internal/deployment/app/query.go
index 98f0cd46..eb7f82b8 100644
--- a/internal/deployment/app/query.go
+++ b/internal/deployment/app/query.go
@@ -9,9 +9,9 @@ type (
}
TargetSummary struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Url string `json:"url"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Url monad.Maybe[string] `json:"url"`
}
LatestDeployments[T any] struct {
diff --git a/internal/deployment/app/update_target/update_target.go b/internal/deployment/app/update_target/update_target.go
index c8e3fa8d..e99c34cd 100644
--- a/internal/deployment/app/update_target/update_target.go
+++ b/internal/deployment/app/update_target/update_target.go
@@ -15,7 +15,7 @@ type Command struct {
ID string `json:"-"`
Name monad.Maybe[string] `json:"name"`
- Url monad.Maybe[string] `json:"url"`
+ Url monad.Patch[string] `json:"url"`
Provider any `json:"-"`
}
@@ -31,7 +31,7 @@ func Handler(
if err := validate.Struct(validate.Of{
"name": validate.Maybe(cmd.Name, strings.Required),
- "url": validate.Maybe(cmd.Url, func(s string) error {
+ "url": validate.Patch(cmd.Url, func(s string) error {
return validate.Value(s, &targetUrl, domain.UrlFrom)
}),
}); err != nil {
@@ -87,8 +87,14 @@ func Handler(
}
}
- if cmd.Url.HasValue() {
- if err = target.HasUrl(urlRequirement); err != nil {
+ if cmd.Url.IsSet() {
+ if cmd.Url.HasValue() {
+ err = target.ExposeServicesAutomatically(urlRequirement)
+ } else {
+ err = target.ExposeServicesManually()
+ }
+
+ if err != nil {
return "", err
}
}
diff --git a/internal/deployment/app/update_target/update_target_test.go b/internal/deployment/app/update_target/update_target_test.go
index 1e2a0612..28d29f29 100644
--- a/internal/deployment/app/update_target/update_target_test.go
+++ b/internal/deployment/app/update_target/update_target_test.go
@@ -40,13 +40,13 @@ func Test_UpdateTarget(t *testing.T) {
config := fixture.ProviderConfig()
targetOne := fixture.Target(
fixture.WithTargetCreatedBy(user.ID()),
- fixture.WithTargetUrl(must.Panic(domain.UrlFrom("http://localhost"))),
fixture.WithProviderConfig(config),
)
+ assert.Nil(t, targetOne.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true)))
targetTwo := fixture.Target(
fixture.WithTargetCreatedBy(user.ID()),
- fixture.WithTargetUrl(must.Panic(domain.UrlFrom("http://docker.localhost"))),
)
+ assert.Nil(t, targetTwo.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
handler, _ := arrange(t,
fixture.WithUsers(&user),
fixture.WithTargets(&targetOne, &targetTwo),
@@ -55,7 +55,7 @@ func Test_UpdateTarget(t *testing.T) {
_, err := handler(context.Background(), update_target.Command{
ID: string(targetTwo.ID()),
Provider: config,
- Url: monad.Value("http://localhost"),
+ Url: monad.PatchValue("http://localhost"),
})
assert.ValidationError(t, validate.FieldErrors{
@@ -64,23 +64,48 @@ func Test_UpdateTarget(t *testing.T) {
}, err)
})
- t.Run("should update the target if everything is good", func(t *testing.T) {
+ t.Run("should be able to remove the url", func(t *testing.T) {
user := authfixture.User()
target := fixture.Target(
fixture.WithTargetCreatedBy(user.ID()),
fixture.WithProviderConfig(fixture.ProviderConfig(fixture.WithFingerprint("test"))),
)
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ handler, dispatcher := arrange(t,
+ fixture.WithUsers(&user),
+ fixture.WithTargets(&target),
+ )
+
+ _, err := handler(context.Background(), update_target.Command{
+ ID: string(target.ID()),
+ Url: monad.Nil[string](),
+ })
+
+ assert.Nil(t, err)
+ assert.HasLength(t, 2, dispatcher.Signals())
+ urlRemoved := assert.Is[domain.TargetUrlRemoved](t, dispatcher.Signals()[0])
+ assert.Equal(t, domain.TargetUrlRemoved{
+ ID: target.ID(),
+ }, urlRemoved)
+ })
+
+ t.Run("should update the target if everything is good", func(t *testing.T) {
+ user := authfixture.User()
+ target := fixture.Target(
+ fixture.WithTargetCreatedBy(user.ID()),
+ fixture.WithProviderConfig(fixture.ProviderConfig(fixture.WithFingerprint("test"), fixture.WithKind("test"))),
+ )
handler, dispatcher := arrange(t,
fixture.WithUsers(&user),
fixture.WithTargets(&target),
)
- newConfig := fixture.ProviderConfig(fixture.WithFingerprint("test"))
+ newConfig := fixture.ProviderConfig(fixture.WithFingerprint("test"), fixture.WithKind("test"))
id, err := handler(context.Background(), update_target.Command{
ID: string(target.ID()),
Name: monad.Value("new name"),
Provider: newConfig,
- Url: monad.Value("http://docker.localhost"),
+ Url: monad.PatchValue("http://docker.localhost"),
})
assert.Nil(t, err)
diff --git a/internal/deployment/domain/service.go b/internal/deployment/domain/service.go
index 20c7dc8e..55b0c61d 100644
--- a/internal/deployment/domain/service.go
+++ b/internal/deployment/domain/service.go
@@ -155,7 +155,7 @@ func (e EntrypointName) Protocol() string {
return string(p)
}
-// Retrieve entrypoints for this service.
+// Retrieve all entrypoints for every services.
func (s Services) Entrypoints() []Entrypoint {
var result []Entrypoint
@@ -166,7 +166,7 @@ func (s Services) Entrypoints() []Entrypoint {
return result
}
-// Retrieve custom entrypoints for this service. Ones that are not natively
+// Retrieve all custom entrypoints. Ones that are not natively
// managed by the target and requires a manual configuration.
func (s Services) CustomEntrypoints() []Entrypoint {
return slices.DeleteFunc(s.Entrypoints(), isNotCustom)
diff --git a/internal/deployment/domain/target.go b/internal/deployment/domain/target.go
index f64cc6bd..14df8015 100644
--- a/internal/deployment/domain/target.go
+++ b/internal/deployment/domain/target.go
@@ -43,7 +43,7 @@ type (
id TargetID
name string
- url Url
+ url monad.Maybe[Url]
provider ProviderConfig
state TargetState
customEntrypoints TargetEntrypoints
@@ -67,7 +67,6 @@ type (
ID TargetID
Name string
- Url Url
Provider ProviderConfig
State TargetState
Entrypoints TargetEntrypoints
@@ -95,6 +94,12 @@ type (
Url Url
}
+ TargetUrlRemoved struct {
+ bus.Notification
+
+ ID TargetID
+ }
+
TargetProviderChanged struct {
bus.Notification
@@ -127,6 +132,7 @@ func (TargetCreated) Name_() string { return "deployment.event.target
func (TargetStateChanged) Name_() string { return "deployment.event.target_state_changed" }
func (TargetRenamed) Name_() string { return "deployment.event.target_renamed" }
func (TargetUrlChanged) Name_() string { return "deployment.event.target_url_changed" }
+func (TargetUrlRemoved) Name_() string { return "deployment.event.target_url_removed" }
func (TargetProviderChanged) Name_() string { return "deployment.event.target_provider_changed" }
func (TargetEntrypointsChanged) Name_() string { return "deployment.event.target_entrypoints_changed" }
func (TargetCleanupRequested) Name_() string { return "deployment.event.target_cleanup_requested" }
@@ -139,16 +145,9 @@ func (e TargetStateChanged) WentToConfiguringState() bool {
// Builds a new deployment target.
func NewTarget(
name string,
- urlRequirement TargetUrlRequirement,
providerRequirement ProviderConfigRequirement,
createdBy auth.UserID,
) (t Target, err error) {
- url, err := urlRequirement.Met()
-
- if err != nil {
- return t, err
- }
-
provider, err := providerRequirement.Met()
if err != nil {
@@ -158,7 +157,6 @@ func NewTarget(
t.apply(TargetCreated{
ID: id.New[TargetID](),
Name: name,
- Url: url.Root(),
Provider: provider,
State: newTargetState(),
Entrypoints: make(TargetEntrypoints),
@@ -229,8 +227,8 @@ func (t *Target) Rename(name string) error {
return nil
}
-// Update the internal domain used by this target.
-func (t *Target) HasUrl(urlRequirement TargetUrlRequirement) error {
+// Mark this target as exposing automatically services on the given root url.
+func (t *Target) ExposeServicesAutomatically(urlRequirement TargetUrlRequirement) error {
if t.cleanupRequested.HasValue() {
return ErrTargetCleanupRequested
}
@@ -241,13 +239,35 @@ func (t *Target) HasUrl(urlRequirement TargetUrlRequirement) error {
return err
}
- if t.url == url {
+ url = url.Root() // Remove path and query part
+
+ if existing, isSet := t.url.TryGet(); isSet && existing == url {
return nil
}
t.apply(TargetUrlChanged{
ID: t.id,
- Url: url.Root(),
+ Url: url,
+ })
+
+ t.reconfigure()
+
+ return nil
+}
+
+// Mark this target as being manually managed by the user. The url will be removed
+// and the user will have to manually manage the proxy configuration.
+func (t *Target) ExposeServicesManually() error {
+ if t.cleanupRequested.HasValue() {
+ return ErrTargetCleanupRequested
+ }
+
+ if !t.url.HasValue() {
+ return nil
+ }
+
+ t.apply(TargetUrlRemoved{
+ ID: t.id,
})
t.reconfigure()
@@ -347,7 +367,7 @@ func (t *Target) Configured(version time.Time, assigned TargetEntrypointsAssigne
// If needed (new or removed entrypoints), a configuration will be triggered.
func (t *Target) ExposeEntrypoints(app AppID, env Environment, services Services) {
// Target is being deleted, no need to reconfigure anything
- if t.cleanupRequested.HasValue() || services == nil {
+ if t.cleanupRequested.HasValue() {
return
}
@@ -461,7 +481,8 @@ func (t *Target) Delete(cleanedUp bool) error {
}
func (t *Target) ID() TargetID { return t.id }
-func (t *Target) Url() Url { return t.url }
+func (t *Target) Url() monad.Maybe[Url] { return t.url }
+func (t *Target) IsManual() bool { return !t.url.HasValue() }
func (t *Target) Provider() ProviderConfig { return t.provider }
func (t *Target) CustomEntrypoints() TargetEntrypoints { return t.customEntrypoints } // FIXME: Should we return a copy?
func (t *Target) CurrentVersion() time.Time { return t.state.version }
@@ -486,6 +507,10 @@ func (t *Target) raiseEntrypointsChangedAndReconfigure() {
Entrypoints: t.customEntrypoints,
})
+ if t.IsManual() {
+ return
+ }
+
t.reconfigure()
}
@@ -494,7 +519,6 @@ func (t *Target) apply(e event.Event) {
case TargetCreated:
t.id = evt.ID
t.name = evt.Name
- t.url = evt.Url
t.provider = evt.Provider
t.state = evt.State
t.created = evt.Created
@@ -502,7 +526,9 @@ func (t *Target) apply(e event.Event) {
case TargetRenamed:
t.name = evt.Name
case TargetUrlChanged:
- t.url = evt.Url
+ t.url.Set(evt.Url)
+ case TargetUrlRemoved:
+ t.url.Unset()
case TargetProviderChanged:
t.provider = evt.Provider
case TargetEntrypointsChanged:
diff --git a/internal/deployment/domain/target_test.go b/internal/deployment/domain/target_test.go
index 95c24b97..9ab7cbaf 100644
--- a/internal/deployment/domain/target_test.go
+++ b/internal/deployment/domain/target_test.go
@@ -15,664 +15,856 @@ import (
)
func Test_Target(t *testing.T) {
-
- t.Run("should fail if the url is not unique", func(t *testing.T) {
- _, err := domain.NewTarget("target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://my-url.com")), false),
- domain.NewProviderConfigRequirement(fixture.ProviderConfig(), true), "uid")
-
- assert.ErrorIs(t, domain.ErrUrlAlreadyTaken, err)
- })
-
- t.Run("should fail if the config is not unique", func(t *testing.T) {
- _, err := domain.NewTarget("target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://my-url.com")), true),
- domain.NewProviderConfigRequirement(fixture.ProviderConfig(), false), "uid")
-
- assert.ErrorIs(t, domain.ErrConfigAlreadyTaken, err)
- })
-
- t.Run("should be instantiable", func(t *testing.T) {
- url := must.Panic(domain.UrlFrom("http://my-url.com"))
- config := fixture.ProviderConfig()
-
- target, err := domain.NewTarget("target",
- domain.NewTargetUrlRequirement(url, true),
- domain.NewProviderConfigRequirement(config, true),
- "uid")
-
- assert.Nil(t, err)
- assert.HasNEvents(t, 1, &target)
- created := assert.EventIs[domain.TargetCreated](t, &target, 0)
-
- assert.DeepEqual(t, domain.TargetCreated{
- ID: assert.NotZero(t, target.ID()),
- Name: "target",
- Url: url,
- Provider: config,
- State: created.State,
- Entrypoints: make(domain.TargetEntrypoints),
- Created: shared.ActionFrom[auth.UserID]("uid", assert.NotZero(t, created.Created.At())),
- }, created)
-
- assert.Equal(t, domain.TargetStatusConfiguring, created.State.Status())
- assert.NotZero(t, created.State.Version())
- })
-
- t.Run("could be renamed and raise the event only if different", func(t *testing.T) {
- target := fixture.Target(fixture.WithTargetName("old-name"))
-
- err := target.Rename("new-name")
-
- assert.Nil(t, err)
- evt := assert.EventIs[domain.TargetRenamed](t, &target, 1)
-
- assert.Equal(t, domain.TargetRenamed{
- ID: target.ID(),
- Name: "new-name",
- }, evt)
-
- assert.Nil(t, target.Rename("new-name"))
- assert.HasNEvents(t, 2, &target, "should have raised the event once")
- })
-
- t.Run("could not be renamed if delete requested", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
-
- assert.Nil(t, target.RequestCleanup(false, "uid"))
-
- assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Rename("new-name"))
- })
-
- t.Run("could have its domain changed if available and raise the event only if different", func(t *testing.T) {
- target := fixture.Target()
- newUrl := must.Panic(domain.UrlFrom("http://new-url.com"))
-
- err := target.HasUrl(domain.NewTargetUrlRequirement(newUrl, false))
- assert.ErrorIs(t, domain.ErrUrlAlreadyTaken, err)
-
- err = target.HasUrl(domain.NewTargetUrlRequirement(newUrl, true))
- assert.Nil(t, err)
- evt := assert.EventIs[domain.TargetUrlChanged](t, &target, 1)
- assert.Equal(t, newUrl, evt.Url)
-
- evtTargetChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
- assert.Equal(t, domain.TargetStatusConfiguring, evtTargetChanged.State.Status())
-
- assert.Nil(t, target.HasUrl(domain.NewTargetUrlRequirement(newUrl, true)))
- assert.HasNEvents(t, 3, &target)
- })
-
- t.Run("could not have its domain changed if delete requested", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
- assert.Nil(t, target.RequestCleanup(false, "uid"))
-
- err := target.HasUrl(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://new-url.com")), true))
-
- assert.ErrorIs(t, domain.ErrTargetCleanupRequested, err)
- })
-
- t.Run("should forbid a provider change if the fingerprint has changed", func(t *testing.T) {
- target := fixture.Target(fixture.WithProviderConfig(fixture.ProviderConfig(fixture.WithFingerprint("docker"))))
-
- err := target.HasProvider(domain.NewProviderConfigRequirement(fixture.ProviderConfig(), true))
-
- assert.ErrorIs(t, domain.ErrTargetProviderUpdateNotPermitted, err)
- })
-
- t.Run("could have its provider changed if available and raise the event only if different", func(t *testing.T) {
- config := fixture.ProviderConfig(fixture.WithFingerprint("docker"))
- target := fixture.Target(fixture.WithProviderConfig(config))
- newConfig := fixture.ProviderConfig(fixture.WithFingerprint("docker"))
-
- err := target.HasProvider(domain.NewProviderConfigRequirement(newConfig, false))
-
- assert.ErrorIs(t, domain.ErrConfigAlreadyTaken, err)
-
- err = target.HasProvider(domain.NewProviderConfigRequirement(newConfig, true))
-
- assert.Nil(t, err)
- evt := assert.EventIs[domain.TargetProviderChanged](t, &target, 1)
- assert.Equal(t, newConfig, evt.Provider)
-
- evtTargetChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
- assert.Equal(t, domain.TargetStatusConfiguring, evtTargetChanged.State.Status())
-
- assert.Nil(t, target.HasProvider(domain.NewProviderConfigRequirement(newConfig, true)))
- assert.HasNEvents(t, 3, &target, "should raise the event only once")
- })
-
- t.Run("could not have its provider changed if delete requested", func(t *testing.T) {
- config := fixture.ProviderConfig(fixture.WithFingerprint("docker"))
- target := fixture.Target(fixture.WithProviderConfig(config))
- target.Configured(target.CurrentVersion(), nil, nil)
-
- assert.Nil(t, target.RequestCleanup(false, "uid"))
- assert.ErrorIs(t, domain.ErrTargetCleanupRequested,
- target.HasProvider(domain.NewProviderConfigRequirement(fixture.ProviderConfig(fixture.WithFingerprint("docker")), true)))
+ // Common data used for custom entrypoints exposure
+ deployment := fixture.Deployment()
+ app := deployment.Config().NewService("app", "app-image")
+ app.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{
+ Managed: true,
})
+ http := app.AddHttpEntrypoint(deployment.Config(), 3000, domain.HttpEntrypointOptions{})
+ db := deployment.Config().NewService("db", "db-image")
+ tcp := db.AddTCPEntrypoint(5432)
+
+ t.Run("could be created", func(t *testing.T) {
+ t.Run("should require a unique provider config", func(t *testing.T) {
+ _, err := domain.NewTarget("target",
+ domain.NewProviderConfigRequirement(fixture.ProviderConfig(), false), "uid")
+
+ assert.ErrorIs(t, domain.ErrConfigAlreadyTaken, err)
+ })
+
+ t.Run("should succeed if everything is good", func(t *testing.T) {
+ config := fixture.ProviderConfig()
+
+ target, err := domain.NewTarget("target",
+ domain.NewProviderConfigRequirement(config, true),
+ "uid")
+
+ assert.Nil(t, err)
+ assert.Equal(t, config, target.Provider())
+ assert.Zero(t, target.Url())
+ assert.HasNEvents(t, 1, &target)
+ created := assert.EventIs[domain.TargetCreated](t, &target, 0)
+
+ assert.DeepEqual(t, domain.TargetCreated{
+ ID: assert.NotZero(t, target.ID()),
+ Name: "target",
+ Provider: config,
+ State: created.State,
+ Entrypoints: make(domain.TargetEntrypoints),
+ Created: shared.ActionFrom[auth.UserID]("uid", assert.NotZero(t, created.Created.At())),
+ }, created)
+
+ assert.Equal(t, domain.TargetStatusConfiguring, created.State.Status())
+ assert.NotZero(t, created.State.Version())
+ })
+ })
+
+ t.Run("should expose a method to check if a version is outdated or not", func(t *testing.T) {
+ t.Run("should return true if the version is outdated", func(t *testing.T) {
+ target := fixture.Target()
+
+ assert.True(t, target.IsOutdated(target.CurrentVersion().Add(-1*time.Second)))
+ })
+
+ t.Run("should return false if the version is not outdated", func(t *testing.T) {
+ target := fixture.Target()
+
+ assert.False(t, target.IsOutdated(target.CurrentVersion()))
+ })
+ })
+
+ t.Run("could be renamed", func(t *testing.T) {
+ t.Run("should not raise the event if the name has not changed", func(t *testing.T) {
+ target := fixture.Target(fixture.WithTargetName("name"))
+
+ assert.Nil(t, target.Rename("name"))
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should raise the event if the name is different", func(t *testing.T) {
+ target := fixture.Target(fixture.WithTargetName("old-name"))
+
+ assert.Nil(t, target.Rename("new-name"))
+ assert.HasNEvents(t, 2, &target)
+ renamed := assert.EventIs[domain.TargetRenamed](t, &target, 1)
+ assert.Equal(t, domain.TargetRenamed{
+ ID: target.ID(),
+ Name: "new-name",
+ }, renamed)
+ })
+
+ t.Run("should returns an error if the target cleanup has been requested", func(t *testing.T) {
+ target := fixture.Target(fixture.WithTargetName("old-name"))
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
+
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Rename("new-name"))
+ })
+ })
+
+ t.Run("could be configured as exposing services automatically with an url", func(t *testing.T) {
+ t.Run("should require the url to be unique", func(t *testing.T) {
+ target := fixture.Target()
+
+ assert.ErrorIs(t, domain.ErrUrlAlreadyTaken, target.ExposeServicesAutomatically(
+ domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), false),
+ ))
+ })
+
+ t.Run("should raise the event if the url is different", func(t *testing.T) {
+ target := fixture.Target()
+ url := must.Panic(domain.UrlFrom("http://example.com"))
+
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(url, true)))
+
+ assert.HasNEvents(t, 3, &target)
+ urlChanged := assert.EventIs[domain.TargetUrlChanged](t, &target, 1)
+ assert.Equal(t, domain.TargetUrlChanged{
+ ID: target.ID(),
+ Url: url,
+ }, urlChanged)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
+
+ t.Run("should not raise the event if the url has not changed", func(t *testing.T) {
+ target := fixture.Target()
+ url := must.Panic(domain.UrlFrom("http://example.com"))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(url, true)))
+
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(url, true)))
+ assert.HasNEvents(t, 3, &target)
+ })
+
+ t.Run("should returns an error if the target cleanup has been requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
+
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.ExposeServicesAutomatically(
+ domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true),
+ ))
+ })
+ })
+
+ t.Run("could be configured as exposing services manually without url", func(t *testing.T) {
+ t.Run("should raise the event if the target had previously an url", func(t *testing.T) {
+ target := fixture.Target()
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true)))
+
+ assert.Nil(t, target.ExposeServicesManually())
- t.Run("could be marked as configured and raise the appropriate event", func(t *testing.T) {
- target := fixture.Target()
+ assert.HasNEvents(t, 5, &target)
+ urlRemoved := assert.EventIs[domain.TargetUrlRemoved](t, &target, 3)
+ assert.Equal(t, domain.TargetUrlRemoved{
+ ID: target.ID(),
+ }, urlRemoved)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 4)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
- target.Configured(target.CurrentVersion().Add(-1*time.Hour), nil, nil)
+ t.Run("should not raise the event if trying to remove an url on a target without one", func(t *testing.T) {
+ target := fixture.Target()
- assert.HasNEvents(t, 1, &target, "should not raise a new event since the version does not match")
- assert.EventIs[domain.TargetCreated](t, &target, 0)
-
- target.Configured(target.CurrentVersion(), nil, nil)
- target.Configured(target.CurrentVersion(), nil, nil) // Should not raise a new event
-
- assert.HasNEvents(t, 2, &target, "should raise the event once")
- changed := assert.EventIs[domain.TargetStateChanged](t, &target, 1)
- assert.Equal(t, domain.TargetStatusReady, changed.State.Status())
- })
-
- t.Run("should handle entrypoints assignment on configuration", func(t *testing.T) {
- target := fixture.Target()
- deployment := fixture.Deployment()
-
- // Assigning non existing entrypoints should just be ignored
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- deployment.ID().AppID(): {
- domain.Production: {
- "non-existing-entrypoint": 5432,
+ assert.Nil(t, target.ExposeServicesManually())
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should returns an error if the target cleanup has been requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
+
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.ExposeServicesManually())
+ })
+ })
+
+ t.Run("could have its provider changed", func(t *testing.T) {
+ t.Run("should require the provider to be unique", func(t *testing.T) {
+ target := fixture.Target()
+
+ assert.ErrorIs(t, domain.ErrConfigAlreadyTaken,
+ target.HasProvider(domain.NewProviderConfigRequirement(fixture.ProviderConfig(), false)))
+ })
+
+ t.Run("should require the fingerprint to be the same", func(t *testing.T) {
+ config := fixture.ProviderConfig(fixture.WithKind("test"), fixture.WithFingerprint("123"))
+ target := fixture.Target(fixture.WithProviderConfig(config))
+
+ assert.ErrorIs(t, domain.ErrTargetProviderUpdateNotPermitted,
+ target.HasProvider(
+ domain.NewProviderConfigRequirement(
+ fixture.ProviderConfig(fixture.WithKind("test"), fixture.WithFingerprint("456")), true)))
+ })
+
+ t.Run("should require the provider kind to be the same", func(t *testing.T) {
+ config := fixture.ProviderConfig(fixture.WithKind("test1"), fixture.WithFingerprint("123"))
+ target := fixture.Target(fixture.WithProviderConfig(config))
+
+ assert.ErrorIs(t, domain.ErrTargetProviderUpdateNotPermitted,
+ target.HasProvider(
+ domain.NewProviderConfigRequirement(
+ fixture.ProviderConfig(fixture.WithKind("test2"), fixture.WithFingerprint("123")), true)))
+ })
+
+ t.Run("should raise the event if the provider is different", func(t *testing.T) {
+ config := fixture.ProviderConfig(fixture.WithKind("test"), fixture.WithFingerprint("123"))
+ target := fixture.Target(fixture.WithProviderConfig(config))
+ newConfig := fixture.ProviderConfig(
+ fixture.WithKind("test"),
+ fixture.WithFingerprint("123"),
+ fixture.WithData("some different data"))
+
+ assert.Nil(t, target.HasProvider(
+ domain.NewProviderConfigRequirement(newConfig, true)))
+ assert.HasNEvents(t, 3, &target)
+ changed := assert.EventIs[domain.TargetProviderChanged](t, &target, 1)
+ assert.Equal(t, domain.TargetProviderChanged{
+ ID: target.ID(),
+ Provider: newConfig,
+ }, changed)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
+
+ t.Run("should not raise the event if the provider is the same", func(t *testing.T) {
+ config := fixture.ProviderConfig(fixture.WithKind("test"), fixture.WithFingerprint("123"))
+ target := fixture.Target(fixture.WithProviderConfig(config))
+
+ assert.Nil(t, target.HasProvider(domain.NewProviderConfigRequirement(config, true)))
+
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should returns an error if the target cleanup has been requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
+
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.HasProvider(domain.NewProviderConfigRequirement(fixture.ProviderConfig(), true)))
+ })
+ })
+
+ t.Run("could expose custom entrypoints", func(t *testing.T) {
+ t.Run("should do nothing if given entrypoints are empty", func(t *testing.T) {
+ target := fixture.Target()
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{})
+
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should do nothing if given entrypoints are nil", func(t *testing.T) {
+ target := fixture.Target()
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, nil)
+
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should add entrypoints", func(t *testing.T) {
+ t.Run("on manual target", func(t *testing.T) {
+ target := fixture.Target()
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+
+ assert.HasNEvents(t, 2, &target)
+ changed := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 1)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): monad.None[domain.Port](),
+ tcp.Name(): monad.None[domain.Port](),
+ },
+ },
+ },
+ }, changed)
+ })
+
+ t.Run("on automatic target", func(t *testing.T) {
+ target := fixture.Target()
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true)))
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+
+ assert.HasNEvents(t, 5, &target)
+ changed := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 3)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): monad.None[domain.Port](),
+ tcp.Name(): monad.None[domain.Port](),
+ },
+ },
+ },
+ }, changed)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 4)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
+ })
+
+ t.Run("should update existing entrypoints", func(t *testing.T) {
+ t.Run("on manual target", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app})
+
+ assert.HasNEvents(t, 3, &target)
+ changed := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 2)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): monad.None[domain.Port](),
+ },
+ },
+ },
+ }, changed)
+ })
+
+ t.Run("on automatic target", func(t *testing.T) {
+ target := fixture.Target()
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true)))
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app})
+
+ assert.HasNEvents(t, 7, &target)
+ changed := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 5)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): monad.None[domain.Port](),
+ },
+ },
+ },
+ }, changed)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 6)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
+ })
+
+ t.Run("should not raise additional events if all entrypoints already exists", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+ assert.HasNEvents(t, 2, &target)
+ })
+
+ t.Run("should be ignored if the target is being configured", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
+
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+
+ assert.HasNEvents(t, 3, &target)
+ })
+ })
+
+ t.Run("could be marked as configured", func(t *testing.T) {
+ t.Run("should do nothing if the version do not match", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+
+ target.Configured(target.CurrentVersion().Add(-1*time.Second), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
},
- },
- }, nil)
+ }, nil)
- assert.HasNEvents(t, 2, &target)
- assert.DeepEqual(t, domain.TargetEntrypoints{}, target.CustomEntrypoints())
+ assert.HasNEvents(t, 2, &target)
+ })
- dbService := deployment.Config().NewService("db", "postgres:14-alpine")
- http := dbService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{})
- tcp := dbService.AddTCPEntrypoint(5432)
+ t.Run("should do nothing if the version has already been configured", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- target.ExposeEntrypoints(deployment.ID().AppID(), domain.Production, domain.Services{dbService})
+ target.Configured(target.CurrentVersion(), nil, nil)
- // Assigning but with an error should ignore new entrypoints
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- deployment.ID().AppID(): {
- domain.Production: {
- http.Name(): 8081,
- tcp.Name(): 8082,
- },
- },
- }, errors.New("some error"))
-
- assert.HasNEvents(t, 5, &target)
- assert.DeepEqual(t, domain.TargetEntrypoints{
- deployment.ID().AppID(): {
- domain.Production: {
- http.Name(): monad.None[domain.Port](),
- tcp.Name(): monad.None[domain.Port](),
- },
- },
- }, target.CustomEntrypoints())
+ assert.HasNEvents(t, 2, &target)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 1)
+ assert.Equal(t, domain.TargetStatusReady, stateChanged.State.Status())
+ })
- assert.Nil(t, target.Reconfigure())
+ t.Run("should be marked as failed if an error is given", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+ err := errors.New("an error")
- // No error, should update the entrypoints correctly
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- deployment.ID().AppID(): {
- domain.Production: {
- http.Name(): 8081,
- tcp.Name(): 8082,
- "non-existing-entrypoint": 5432,
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
},
- "non-existing-env": {
- "non-existing-entrypoint": 5432,
+ }, err)
+
+ assert.HasNEvents(t, 3, &target)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
+ assert.Equal(t, domain.TargetStatusFailed, stateChanged.State.Status())
+ assert.Equal(t, err.Error(), stateChanged.State.ErrCode().Get(""))
+ })
+
+ t.Run("should be marked as ready and update entrypoints with given assigned ports ignoring non-existing entrypoints", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app, db})
+
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ "another-app": {
+ domain.Production: {
+ "some-entrypoint": 5000,
+ },
},
- },
- "another-app": {
- "non-existing-env": {
- "non-existing-entrypoint": 5432,
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
},
- },
- }, nil)
-
- assert.HasNEvents(t, 8, &target)
- assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 6)
- changed := assert.EventIs[domain.TargetStateChanged](t, &target, 7)
- assert.Equal(t, domain.TargetStatusReady, changed.State.Status())
- assert.DeepEqual(t, domain.TargetEntrypoints{
- deployment.ID().AppID(): {
- domain.Production: {
- http.Name(): monad.Value[domain.Port](8081),
- tcp.Name(): monad.Value[domain.Port](8082),
+ }, nil)
+
+ assert.HasNEvents(t, 5, &target)
+ entrypointsChanged := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 3)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Staging: {
+ http.Name(): monad.None[domain.Port](),
+ tcp.Name(): monad.None[domain.Port](),
+ },
+ domain.Production: {
+ http.Name(): monad.Value[domain.Port](3000),
+ tcp.Name(): monad.Value[domain.Port](3001),
+ },
+ },
},
- },
- }, target.CustomEntrypoints())
- })
-
- t.Run("should be able to unexpose entrypoints for a specific app", func(t *testing.T) {
- target := fixture.Target()
- deployment := fixture.Deployment()
- dbService := deployment.Config().NewService("db", "postgres:14-alpine")
- http := dbService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{})
- tcp := dbService.AddTCPEntrypoint(5432)
-
- target.UnExposeEntrypoints(deployment.ID().AppID())
-
- assert.HasNEvents(t, 1, &target, "should not raise an event since no entrypoints were exposed")
-
- target.ExposeEntrypoints(deployment.ID().AppID(), domain.Production, domain.Services{dbService})
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- deployment.ID().AppID(): {
- domain.Production: {
- http.Name(): 8081,
- tcp.Name(): 8082,
+ }, entrypointsChanged)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 4)
+ assert.Equal(t, domain.TargetStatusReady, stateChanged.State.Status())
+ })
+ })
+
+ t.Run("could un-expose custom entrypoints", func(t *testing.T) {
+ t.Run("should do nothing if not previously exposed", func(t *testing.T) {
+ target := fixture.Target()
+
+ target.UnExposeEntrypoints(deployment.Config().AppID(), domain.Production)
+
+ assert.HasNEvents(t, 1, &target)
+ })
+
+ t.Run("should un-expose all entrypoints of a given application", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints("app", domain.Production, domain.Services{app, db})
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app, db})
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
+ domain.Staging: {
+ http.Name(): 3002,
+ tcp.Name(): 3003,
+ },
},
- },
- }, nil)
-
- target.UnExposeEntrypoints(deployment.ID().AppID())
-
- assert.HasNEvents(t, 7, &target)
- assert.DeepEqual(t, domain.TargetEntrypoints{}, target.CustomEntrypoints())
- changed := assert.EventIs[domain.TargetStateChanged](t, &target, 6)
- assert.Equal(t, domain.TargetStatusConfiguring, changed.State.Status())
-
- target.ExposeEntrypoints(deployment.ID().AppID(), domain.Production, domain.Services{dbService})
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- deployment.ID().AppID(): {
- domain.Production: {
- http.Name(): 8081,
- tcp.Name(): 8082,
+ }, nil)
+
+ target.UnExposeEntrypoints(deployment.Config().AppID())
+
+ assert.HasNEvents(t, 7, &target)
+ entrypointsChanged := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 6)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ "app": {
+ domain.Production: {
+ http.Name(): monad.None[domain.Port](),
+ tcp.Name(): monad.None[domain.Port](),
+ },
+ },
},
- },
- }, nil)
+ }, entrypointsChanged)
+ })
+
+ t.Run("should un-expose all entrypoints of an application for a specific environment", func(t *testing.T) {
+ t.Run("on manual target", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app, db})
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
+ domain.Staging: {
+ http.Name(): 3002,
+ tcp.Name(): 3003,
+ },
+ },
+ }, nil)
+
+ target.UnExposeEntrypoints(deployment.Config().AppID(), domain.Production)
+
+ assert.HasNEvents(t, 6, &target)
+ entrypointsChanged := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 5)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Staging: {
+ http.Name(): monad.Value[domain.Port](3002),
+ tcp.Name(): monad.Value[domain.Port](3003),
+ },
+ },
+ },
+ }, entrypointsChanged)
+ })
+
+ t.Run("on automatic target", func(t *testing.T) {
+ target := fixture.Target()
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("https://example.com")), true)))
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app, db})
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
+ domain.Staging: {
+ http.Name(): 3002,
+ tcp.Name(): 3003,
+ },
+ },
+ }, nil)
+
+ target.UnExposeEntrypoints(deployment.Config().AppID(), domain.Production)
+
+ assert.HasNEvents(t, 11, &target)
+ entrypointsChanged := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 9)
+ assert.DeepEqual(t, domain.TargetEntrypointsChanged{
+ ID: target.ID(),
+ Entrypoints: domain.TargetEntrypoints{
+ deployment.Config().AppID(): {
+ domain.Staging: {
+ http.Name(): monad.Value[domain.Port](3002),
+ tcp.Name(): monad.Value[domain.Port](3003),
+ },
+ },
+ },
+ }, entrypointsChanged)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 10)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
+ })
+
+ t.Run("should be ignored if the target cleanup has been requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app, db})
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.Config().AppID(): {
+ domain.Production: {
+ http.Name(): 3000,
+ tcp.Name(): 3001,
+ },
+ },
+ }, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- target.UnExposeEntrypoints(deployment.ID().AppID(), domain.Staging)
- target.UnExposeEntrypoints(deployment.ID().AppID(), domain.Production)
+ target.UnExposeEntrypoints(deployment.Config().AppID(), domain.Production)
- assert.HasNEvents(t, 13, &target)
- assert.DeepEqual(t, domain.TargetEntrypoints{}, target.CustomEntrypoints())
- changed = assert.EventIs[domain.TargetStateChanged](t, &target, 12)
- assert.Equal(t, domain.TargetStatusConfiguring, changed.State.Status())
+ assert.HasNEvents(t, 5, &target)
+ })
})
t.Run("could expose its availability based on its internal state", func(t *testing.T) {
- target := fixture.Target()
-
- // Configuring
- err := target.CheckAvailability()
+ t.Run("when configuring", func(t *testing.T) {
+ target := fixture.Target()
- assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.CheckAvailability())
+ })
- // Configuration failed
- target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
+ t.Run("when configuration failed", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
- err = target.CheckAvailability()
+ assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, target.CheckAvailability())
+ })
- assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
+ t.Run("when ready", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- // Configuration success
- assert.Nil(t, target.Reconfigure())
+ assert.Nil(t, target.CheckAvailability())
+ })
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("when cleanup requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- err = target.CheckAvailability()
-
- assert.Nil(t, err)
-
- // Delete requested
- assert.Nil(t, target.RequestCleanup(false, "uid"))
-
- err = target.CheckAvailability()
-
- assert.ErrorIs(t, domain.ErrTargetCleanupRequested, err)
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.CheckAvailability())
+ })
})
- t.Run("could not be reconfigured if cleanup requested", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
- assert.Nil(t, target.RequestCleanup(false, "uid"))
+ t.Run("could be reconfigured", func(t *testing.T) {
+ t.Run("should fail if already being configured", func(t *testing.T) {
+ target := fixture.Target()
- assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Reconfigure())
- })
-
- t.Run("could not be reconfigured if configuring", func(t *testing.T) {
- target := fixture.Target()
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.Reconfigure())
+ })
- assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.Reconfigure())
- })
+ t.Run("should fail if cleanup requested", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- t.Run("should not be removed if still used by an app", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
+ assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Reconfigure())
+ })
- assert.ErrorIs(t, domain.ErrTargetInUse, target.RequestCleanup(true, "uid"))
- })
+ t.Run("should succeed otherwise", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- t.Run("should not be removed if configuring", func(t *testing.T) {
- target := fixture.Target()
+ assert.Nil(t, target.Reconfigure())
- assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.RequestCleanup(false, "uid"))
+ assert.HasNEvents(t, 3, &target)
+ stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
+ assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status())
+ })
})
- t.Run("could be removed if no app is using it", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("could be marked for cleanup", func(t *testing.T) {
+ t.Run("should returns an err if some applications are using it", func(t *testing.T) {
+ target := fixture.Target()
- err := target.RequestCleanup(false, "uid")
+ assert.ErrorIs(t, domain.ErrTargetInUse, target.RequestCleanup(true, "uid"))
+ })
- assert.Nil(t, err)
- assert.HasNEvents(t, 3, &target)
- evt := assert.EventIs[domain.TargetCleanupRequested](t, &target, 2)
+ t.Run("should returns an err if configuring", func(t *testing.T) {
+ target := fixture.Target()
- assert.Equal(t, domain.TargetCleanupRequested{
- ID: target.ID(),
- Requested: shared.ActionFrom[auth.UserID]("uid", assert.NotZero(t, evt.Requested.At())),
- }, evt)
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.RequestCleanup(false, "uid"))
+ })
- t.Run("should not raise an event if the target is already marked has deleting", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("should succeed otherwise", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- assert.Nil(t, target.RequestCleanup(false, "uid"))
- assert.Nil(t, target.RequestCleanup(false, "uid"))
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- assert.HasNEvents(t, 3, &target)
- })
+ assert.HasNEvents(t, 3, &target)
+ requested := assert.EventIs[domain.TargetCleanupRequested](t, &target, 2)
+ assert.Equal(t, domain.TargetCleanupRequested{
+ ID: target.ID(),
+ Requested: shared.ActionFrom[auth.UserID]("uid", assert.NotZero(t, requested.Requested.At())),
+ }, requested)
+ })
- t.Run("should returns an err if trying to cleanup a target while configuring", func(t *testing.T) {
- target := fixture.Target()
+ t.Run("should do nothing if already being cleaned up", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- _, err := target.CleanupStrategy(false)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
+ assert.HasNEvents(t, 3, &target)
+ })
})
- t.Run("should returns an err if trying to cleanup a target while deployments are still running", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("should expose a cleanup strategy to determine how the target resources should be handled", func(t *testing.T) {
+ t.Run("should returns an error if there are running or pending deployments on the target", func(t *testing.T) {
+ target := fixture.Target()
- _, err := target.CleanupStrategy(true)
+ _, err := target.CleanupStrategy(true)
- assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err)
- })
+ assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err)
+ })
- t.Run("should returns the skip cleanup strategy if the configuration has failed and the target could not be updated anymore", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
- assert.Nil(t, target.Reconfigure())
- target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
- assert.Nil(t, target.RequestCleanup(false, "uid"))
+ t.Run("should returns an error if the target is being configured", func(t *testing.T) {
+ target := fixture.Target()
- s, err := target.CleanupStrategy(false)
+ _, err := target.CleanupStrategy(false)
- assert.Nil(t, err)
- assert.Equal(t, domain.CleanupStrategySkip, s)
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
+ })
- t.Run("should returns the skip cleanup strategy if the configuration has failed and has never been reachable", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
+ t.Run("should returns an error if the target configuration has failed and it has been at least ready once", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.Reconfigure())
+ target.Configured(target.CurrentVersion(), nil, errors.New("failed"))
- s, err := target.CleanupStrategy(false)
+ _, err := target.CleanupStrategy(false)
- assert.Nil(t, err)
- assert.Equal(t, domain.CleanupStrategySkip, s)
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
+ })
- t.Run("should returns an err if the configuration has failed but the target is still updatable", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
- assert.Nil(t, target.Reconfigure())
- target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
+ t.Run("should returns the skip strategy if the target has never been correctly configured and is currently failing", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, errors.New("failed"))
- _, err := target.CleanupStrategy(false)
+ strategy, err := target.CleanupStrategy(false)
- assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
- })
+ assert.Nil(t, err)
+ assert.Equal(t, domain.CleanupStrategySkip, strategy)
+ })
- t.Run("should returns the default strategy if the target is correctly configured", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("should returns the default strategy if the target is ready", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- s, err := target.CleanupStrategy(false)
+ strategy, err := target.CleanupStrategy(false)
- assert.Nil(t, err)
- assert.Equal(t, domain.CleanupStrategyDefault, s)
+ assert.Nil(t, err)
+ assert.Equal(t, domain.CleanupStrategyDefault, strategy)
+ })
})
- t.Run("returns an err if trying to cleanup an app while configuring", func(t *testing.T) {
- target := fixture.Target()
+ t.Run("should expose an application cleanup strategy to determine how application resources should be handled", func(t *testing.T) {
+ t.Run("should returns the skip strategy if the target is being cleaned up", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- _, err := target.AppCleanupStrategy(false, true)
+ strategy, err := target.AppCleanupStrategy(false, true)
- assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
- })
+ assert.Nil(t, err)
+ assert.Equal(t, domain.CleanupStrategySkip, strategy)
+ })
- t.Run("returns a skip strategy when trying to cleanup an app on a deleting target", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
- assert.Nil(t, target.RequestCleanup(false, "uid"))
+ t.Run("should returns an error if there are still running deployments on the target for this application", func(t *testing.T) {
+ target := fixture.Target()
- s, err := target.AppCleanupStrategy(false, false)
+ _, err := target.AppCleanupStrategy(true, true)
- assert.Nil(t, err)
- assert.Equal(t, domain.CleanupStrategySkip, s)
- })
+ assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err)
+ })
- t.Run("returns a skip strategy when trying to cleanup an app when no successful deployment has been made", func(t *testing.T) {
- target := fixture.Target()
+ t.Run("should returns the skip strategy if no successful deployment has been made and no one is running", func(t *testing.T) {
+ target := fixture.Target()
- s, err := target.AppCleanupStrategy(false, false)
+ strategy, err := target.AppCleanupStrategy(false, false)
- assert.Nil(t, err)
- assert.Equal(t, domain.CleanupStrategySkip, s)
- })
+ assert.Nil(t, err)
+ assert.Equal(t, domain.CleanupStrategySkip, strategy)
+ })
- t.Run("returns an error when trying to cleanup an app on a failed target", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
- assert.Nil(t, target.Reconfigure())
- target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed"))
+ t.Run("should returns an error if the target is being configured", func(t *testing.T) {
+ target := fixture.Target()
- _, err := target.AppCleanupStrategy(false, true)
+ _, err := target.AppCleanupStrategy(false, true)
- assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err)
+ })
- t.Run("returns an error when trying to cleanup an app but there are still running or pending deployments", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("should returns an error if the target configuration has failed", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, errors.New("failed"))
- _, err := target.AppCleanupStrategy(true, false)
+ _, err := target.AppCleanupStrategy(false, true)
- assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err)
- })
+ assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err)
+ })
- t.Run("returns a default strategy when trying to remove an app and everything is good to process it", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
+ t.Run("should returns the default strategy if the target is ready", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- s, err := target.AppCleanupStrategy(false, true)
+ strategy, err := target.AppCleanupStrategy(false, true)
- assert.Nil(t, err)
- assert.Equal(t, domain.CleanupStrategyDefault, s)
+ assert.Nil(t, err)
+ assert.Equal(t, domain.CleanupStrategyDefault, strategy)
+ })
})
- t.Run("should do nothing if trying to expose an empty entrypoints array", func(t *testing.T) {
- target := fixture.Target()
+ t.Run("could be deleted", func(t *testing.T) {
+ t.Run("should returns an error if the target has not been mark for cleanup", func(t *testing.T) {
+ target := fixture.Target()
- target.ExposeEntrypoints("appid", domain.Production, domain.Services{})
- assert.HasNEvents(t, 1, &target)
+ assert.ErrorIs(t, domain.ErrTargetCleanupNeeded, target.Delete(true))
+ })
- target.ExposeEntrypoints("appid", domain.Production, nil)
- assert.HasNEvents(t, 1, &target)
- })
+ t.Run("should returns an error if the target resources has not been cleaned up", func(t *testing.T) {
+ target := fixture.Target()
- t.Run("should switch to the configuring state if adding new entrypoints to expose", func(t *testing.T) {
- target := fixture.Target()
- deployment := fixture.Deployment()
- appService := deployment.Config().NewService("app", "")
- http := appService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{})
- udp := appService.AddUDPEntrypoint(8080)
- dbService := deployment.Config().NewService("db", "postgres:14-alpine")
- tcp := dbService.AddTCPEntrypoint(5432)
-
- services := domain.Services{appService, dbService}
-
- target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), services)
-
- assert.HasNEvents(t, 3, &target)
- evt := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 1)
- assert.DeepEqual(t, domain.TargetEntrypoints{
- deployment.ID().AppID(): {
- deployment.Config().Environment(): {
- http.Name(): monad.None[domain.Port](),
- udp.Name(): monad.None[domain.Port](),
- tcp.Name(): monad.None[domain.Port](),
- },
- },
- }, evt.Entrypoints)
+ assert.ErrorIs(t, domain.ErrTargetCleanupNeeded, target.Delete(false))
+ })
- changed := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
- assert.Equal(t, domain.TargetStatusConfiguring, changed.State.Status())
+ t.Run("should succeed otherwise", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.RequestCleanup(false, "uid"))
- // Should not trigger it again
- target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), services)
- assert.HasNEvents(t, 3, &target)
+ assert.Nil(t, target.Delete(true))
+ assert.HasNEvents(t, 4, &target)
+ deleted := assert.EventIs[domain.TargetDeleted](t, &target, 3)
+ assert.Equal(t, domain.TargetDeleted{
+ ID: target.ID(),
+ }, deleted)
+ })
})
+}
- t.Run("should switch to the configuring state if adding new entrypoints to an already exposed environment", func(t *testing.T) {
- target := fixture.Target()
- deployment := fixture.Deployment()
- appService := deployment.Config().NewService("app", "")
- http := appService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{})
-
- target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{appService})
-
- assert.HasNEvents(t, 3, &target)
- evt := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 1)
- assert.DeepEqual(t, domain.TargetEntrypoints{
- deployment.ID().AppID(): {
- deployment.Config().Environment(): {
- http.Name(): monad.None[domain.Port](),
- },
- },
- }, evt.Entrypoints)
+func Test_TargetEvents(t *testing.T) {
+ t.Run("should provide a function to check for configuration changes", func(t *testing.T) {
+ t.Run("should return false if the state is not configuring", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
- // Adding a new entrypoint should trigger new events
- dbService := deployment.Config().NewService("db", "postgres:14-alpine")
- tcp := dbService.AddTCPEntrypoint(5432)
+ evt := assert.EventIs[domain.TargetStateChanged](t, &target, 1)
+ assert.False(t, evt.WentToConfiguringState())
+ })
- target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{appService, dbService})
+ t.Run("should return true if going to the configuring state", func(t *testing.T) {
+ target := fixture.Target()
+ target.Configured(target.CurrentVersion(), nil, nil)
+ assert.Nil(t, target.Reconfigure())
- assert.HasNEvents(t, 5, &target)
- evt = assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 3)
- assert.DeepEqual(t, domain.TargetEntrypoints{
- deployment.ID().AppID(): {
- deployment.Config().Environment(): {
- http.Name(): monad.None[domain.Port](),
- tcp.Name(): monad.None[domain.Port](),
- },
- },
- }, evt.Entrypoints)
-
- // Again with the same entrypoints, should trigger nothing new
- target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{appService, dbService, deployment.Config().NewService("cache", "redis:6-alpine")})
- assert.HasNEvents(t, 5, &target)
+ evt := assert.EventIs[domain.TargetStateChanged](t, &target, 2)
+ assert.True(t, evt.WentToConfiguringState())
+ })
})
+}
- t.Run("should switch to the configuring state if removing entrypoints", func(t *testing.T) {
- target := fixture.Target()
- deployment := fixture.Deployment()
- appService := deployment.Config().NewService("app", "")
- http := appService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{})
- appService.AddUDPEntrypoint(8080)
- dbService := deployment.Config().NewService("db", "postgres:14-alpine")
- tcp := dbService.AddTCPEntrypoint(5432)
-
- target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{appService, dbService})
-
- // Let's remove the UDP entrypoint
- appService = deployment.Config().NewService("app", "")
- appService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{})
-
- target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{appService, dbService})
-
- assert.HasNEvents(t, 5, &target)
- evt := assert.EventIs[domain.TargetEntrypointsChanged](t, &target, 3)
- assert.DeepEqual(t, domain.TargetEntrypoints{
- deployment.ID().AppID(): {
- deployment.Config().Environment(): {
- http.Name(): monad.None[domain.Port](),
- tcp.Name(): monad.None[domain.Port](),
- },
- },
- }, evt.Entrypoints)
- })
+func Test_TargetEntrypointsAssigned(t *testing.T) {
+ t.Run("should provide a function to set entrypoints values", func(t *testing.T) {
+ assigned := make(domain.TargetEntrypointsAssigned)
- t.Run("should remove empty map keys when updating entrypoints", func(t *testing.T) {
- target := fixture.Target()
- deployment := fixture.Deployment()
- appService := deployment.Config().NewService("app", "")
- http := appService.AddHttpEntrypoint(deployment.Config(), 80, domain.HttpEntrypointOptions{})
- tcp := appService.AddTCPEntrypoint(5432)
+ assigned.Set("app", domain.Production, "http", 3000)
+ assigned.Set("app", domain.Production, "tcp", 3001)
+ assigned.Set("app", domain.Staging, "http", 3002)
- target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{appService})
- assert.DeepEqual(t, domain.TargetEntrypoints{
- deployment.ID().AppID(): {
+ assert.DeepEqual(t, domain.TargetEntrypointsAssigned{
+ "app": {
domain.Production: {
- http.Name(): monad.None[domain.Port](),
- tcp.Name(): monad.None[domain.Port](),
+ "http": 3000,
+ "tcp": 3001,
+ },
+ domain.Staging: {
+ "http": 3002,
},
},
- }, target.CustomEntrypoints())
-
- target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{})
-
- assert.DeepEqual(t, domain.TargetEntrypoints{}, target.CustomEntrypoints())
- })
-
- t.Run("should not be removed if no cleanup request has been set", func(t *testing.T) {
- target := fixture.Target()
-
- err := target.Delete(true)
-
- assert.ErrorIs(t, domain.ErrTargetCleanupNeeded, err)
- })
-
- t.Run("should not be removed if target resources have not been cleaned up", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
- assert.Nil(t, target.RequestCleanup(false, "uid")) // No application is using it
-
- err := target.Delete(false)
-
- assert.ErrorIs(t, domain.ErrTargetCleanupNeeded, err)
- })
-
- t.Run("could be removed if resources have been cleaned up", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
- assert.Nil(t, target.RequestCleanup(false, "uid"))
-
- err := target.Delete(true)
-
- assert.Nil(t, err)
- assert.EventIs[domain.TargetDeleted](t, &target, 3)
- })
-}
-
-func Test_TargetEvents(t *testing.T) {
- t.Run("TargetStateChanged should provide a function to check for configuration changes", func(t *testing.T) {
- target := fixture.Target()
- target.Configured(target.CurrentVersion(), nil, nil)
-
- evt := assert.EventIs[domain.TargetStateChanged](t, &target, 1)
- assert.False(t, evt.WentToConfiguringState())
-
- assert.Nil(t, target.Reconfigure())
-
- evt = assert.EventIs[domain.TargetStateChanged](t, &target, 2)
- assert.True(t, evt.WentToConfiguringState())
+ }, assigned)
})
}
diff --git a/internal/deployment/fixture/deployment.go b/internal/deployment/fixture/deployment.go
index f498c0c1..581f0140 100644
--- a/internal/deployment/fixture/deployment.go
+++ b/internal/deployment/fixture/deployment.go
@@ -44,6 +44,12 @@ func FromApp(app domain.App) DeploymentOptionBuilder {
}
}
+func WithSourceData(source domain.SourceData) DeploymentOptionBuilder {
+ return func(o *deploymentOption) {
+ o.source = source
+ }
+}
+
func WithDeploymentRequestedBy(uid auth.UserID) DeploymentOptionBuilder {
return func(o *deploymentOption) {
o.uid = uid
diff --git a/internal/deployment/fixture/target.go b/internal/deployment/fixture/target.go
index 66c1f74e..fb3076b8 100644
--- a/internal/deployment/fixture/target.go
+++ b/internal/deployment/fixture/target.go
@@ -15,7 +15,6 @@ import (
type (
targetOption struct {
name string
- url domain.Url
provider domain.ProviderConfig
uid auth.UserID
}
@@ -26,7 +25,6 @@ type (
func Target(options ...TargetOptionBuilder) domain.Target {
opts := targetOption{
name: id.New[string](),
- url: must.Panic(domain.UrlFrom("http://" + id.New[string]() + ".com")),
provider: ProviderConfig(),
uid: id.New[auth.UserID](),
}
@@ -36,7 +34,6 @@ func Target(options ...TargetOptionBuilder) domain.Target {
}
return must.Panic(domain.NewTarget(opts.name,
- domain.NewTargetUrlRequirement(opts.url, true),
domain.NewProviderConfigRequirement(opts.provider, true),
opts.uid))
}
@@ -53,12 +50,6 @@ func WithTargetCreatedBy(uid auth.UserID) TargetOptionBuilder {
}
}
-func WithTargetUrl(url domain.Url) TargetOptionBuilder {
- return func(opts *targetOption) {
- opts.url = url
- }
-}
-
func WithProviderConfig(config domain.ProviderConfig) TargetOptionBuilder {
return func(opts *targetOption) {
opts.provider = config
@@ -67,6 +58,7 @@ func WithProviderConfig(config domain.ProviderConfig) TargetOptionBuilder {
type (
providerConfig struct {
+ Kind_ string
Data string
Fingerprint_ string
}
@@ -74,9 +66,10 @@ type (
ProviderConfigBuilder func(*providerConfig)
)
-func ProviderConfig(options ...ProviderConfigBuilder) domain.ProviderConfig {
+func ProviderConfig(options ...ProviderConfigBuilder) (result domain.ProviderConfig) {
config := providerConfig{
Data: id.New[string](),
+ Kind_: id.New[string](),
Fingerprint_: id.New[string](),
}
@@ -84,7 +77,18 @@ func ProviderConfig(options ...ProviderConfigBuilder) domain.ProviderConfig {
o(&config)
}
- return config
+ result = config
+
+ // Just ignore the panic due to the multiple registration of same kind
+ defer func() {
+ _ = recover()
+ }()
+
+ domain.ProviderConfigTypes.Register(config, func(s string) (domain.ProviderConfig, error) {
+ return storage.UnmarshalJSON[providerConfig](s)
+ })
+
+ return
}
func WithFingerprint(fingerprint string) ProviderConfigBuilder {
@@ -93,7 +97,19 @@ func WithFingerprint(fingerprint string) ProviderConfigBuilder {
}
}
-func (d providerConfig) Kind() string { return "test" }
+func WithKind(kind string) ProviderConfigBuilder {
+ return func(config *providerConfig) {
+ config.Kind_ = kind
+ }
+}
+
+func WithData(data string) ProviderConfigBuilder {
+ return func(config *providerConfig) {
+ config.Data = data
+ }
+}
+
+func (d providerConfig) Kind() string { return d.Kind_ }
func (d providerConfig) Fingerprint() string { return d.Fingerprint_ }
func (d providerConfig) String() string { return d.Fingerprint_ }
func (d providerConfig) Value() (driver.Value, error) { return storage.ValueJSON(d) }
@@ -101,9 +117,3 @@ func (d providerConfig) Value() (driver.Value, error) { return storage.ValueJSON
func (d providerConfig) Equals(other domain.ProviderConfig) bool {
return d == other
}
-
-func init() {
- domain.ProviderConfigTypes.Register(providerConfig{}, func(s string) (domain.ProviderConfig, error) {
- return storage.UnmarshalJSON[providerConfig](s)
- })
-}
diff --git a/internal/deployment/infra/artifact/local_artifact_manager.go b/internal/deployment/infra/artifact/local_artifact_manager.go
index fa88c12b..5a2f150f 100644
--- a/internal/deployment/infra/artifact/local_artifact_manager.go
+++ b/internal/deployment/infra/artifact/local_artifact_manager.go
@@ -55,16 +55,16 @@ func NewLocal(options LocalOptions, logger log.Logger) domain.ArtifactManager {
func (a *localArtifactManager) PrepareBuild(
ctx context.Context,
- depl domain.Deployment,
+ deployment domain.Deployment,
) (domain.DeploymentContext, error) {
- logfile, err := ostools.OpenAppend(a.LogPath(ctx, depl))
+ logFile, err := ostools.OpenAppend(a.LogPath(ctx, deployment))
if err != nil {
a.logger.Error(err)
return domain.DeploymentContext{}, ErrArtifactOpenLoggerFailed
}
- logger := newLogger(logfile)
+ logger := newLogger(logFile)
defer func() {
if err == nil {
@@ -76,7 +76,7 @@ func (a *localArtifactManager) PrepareBuild(
logger.Close() // And close the logger right now
}()
- buildDirectory, err := a.deploymentPath(depl)
+ buildDirectory, err := a.deploymentPath(deployment)
if err != nil {
return domain.DeploymentContext{}, err
diff --git a/internal/deployment/infra/provider/docker/client.go b/internal/deployment/infra/provider/docker/client.go
index 6951f8c6..3158fe18 100644
--- a/internal/deployment/infra/provider/docker/client.go
+++ b/internal/deployment/infra/provider/docker/client.go
@@ -12,10 +12,10 @@ import (
"github.com/docker/cli/cli/flags"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
- "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
+ "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
dclient "github.com/docker/docker/client"
)
@@ -142,7 +142,7 @@ func (c *client) RemoveResources(ctx context.Context, criteria filters.Args) err
}
// List and remove all networks
- networks, err := c.api.NetworkList(ctx, types.NetworkListOptions{
+ networks, err := c.api.NetworkList(ctx, network.ListOptions{
Filters: criteria,
})
diff --git a/internal/deployment/infra/provider/docker/deployment.go b/internal/deployment/infra/provider/docker/deployment.go
index 09f7c07e..3f53ad10 100644
--- a/internal/deployment/infra/provider/docker/deployment.go
+++ b/internal/deployment/infra/provider/docker/deployment.go
@@ -18,23 +18,35 @@ import (
"golang.org/x/exp/maps"
)
-type deploymentProjectBuilder struct {
- sourceDir string
- composePath string
- networkName string
- services domain.Services
- project *types.Project
- config domain.DeploymentConfig
- logger domain.DeploymentLogger
- labels types.Labels
- isDefaultSubdomainAvailable bool
- routersByPort map[string]domain.Router
-}
+type (
+ DeploymentProjectBuilder interface {
+ Build(context.Context) (*types.Project, domain.Services, error)
+ }
+
+ deploymentProjectBuilder struct {
+ exposedManually bool
+ sourceDir string
+ composePath string
+ networkName string
+ services domain.Services
+ project *types.Project
+ config domain.DeploymentConfig
+ logger domain.DeploymentLogger
+ labels types.Labels
+ isDefaultSubdomainAvailable bool
+ routersByPort map[string]domain.Router
+ }
+)
-func newDeploymentProjectBuilder(ctx domain.DeploymentContext, depl domain.Deployment) *deploymentProjectBuilder {
- config := depl.Config()
+func newDeploymentProjectBuilder(
+ ctx domain.DeploymentContext,
+ deployment domain.Deployment,
+ target domain.Target,
+) DeploymentProjectBuilder {
+ config := deployment.Config()
return &deploymentProjectBuilder{
+ exposedManually: target.IsManual(),
isDefaultSubdomainAvailable: true,
sourceDir: ctx.BuildDirectory(),
config: config,
@@ -42,7 +54,7 @@ func newDeploymentProjectBuilder(ctx domain.DeploymentContext, depl domain.Deplo
logger: ctx.Logger(),
routersByPort: make(map[string]domain.Router),
labels: types.Labels{
- AppLabel: string(depl.ID().AppID()),
+ AppLabel: string(deployment.ID().AppID()),
TargetLabel: string(config.Target()),
EnvironmentLabel: string(config.Environment()),
},
@@ -111,11 +123,14 @@ func (b *deploymentProjectBuilder) findComposeFile() error {
func (b *deploymentProjectBuilder) loadProject(ctx context.Context) error {
b.logger.Stepf("reading project from %s", b.composePath)
- opts, err := cli.NewProjectOptions([]string{b.composePath},
+ loaders := []cli.ProjectOptionsFn{
cli.WithName(b.config.ProjectName()),
cli.WithNormalization(true),
cli.WithProfiles([]string{string(b.config.Environment())}),
- cli.WithLoadOptions(func(o *loader.Options) {
+ }
+
+ if !b.exposedManually {
+ loaders = append(loaders, cli.WithLoadOptions(func(o *loader.Options) {
o.Interpolate = &interpolation.Options{
TypeCastMapping: map[tree.Path]interpolation.Cast{
"services.*.ports.[]": func(value string) (any, error) {
@@ -123,8 +138,10 @@ func (b *deploymentProjectBuilder) loadProject(ctx context.Context) error {
},
},
}
- }),
- )
+ }))
+ }
+
+ opts, err := cli.NewProjectOptions([]string{b.composePath}, loaders...)
if err != nil {
b.logger.Error(err)
@@ -155,7 +172,7 @@ func (b *deploymentProjectBuilder) transform() {
}
// Let's transform the project to expose needed services
- // Here ServiceNames sort the services by alphabetical order
+ // Here ServiceNames sort the services by alphabetical order so we don't have to
for _, name := range b.project.ServiceNames() {
serviceDefinition := b.project.Services[name]
service := b.config.NewService(serviceDefinition.Name, serviceDefinition.Image)
@@ -195,8 +212,8 @@ func (b *deploymentProjectBuilder) transform() {
}
}
- // No ports mapped, nothing to do
- if len(serviceDefinition.Ports) == 0 {
+ // No ports mapped or manual target, nothing to do
+ if b.exposedManually || len(serviceDefinition.Ports) == 0 {
b.project.Services[serviceName] = serviceDefinition
b.services = append(b.services, service)
continue
@@ -276,6 +293,9 @@ func (b *deploymentProjectBuilder) transform() {
}
// Append the public seelf network to the project
+ if b.exposedManually {
+ return
+ }
if b.project.Networks == nil {
b.project.Networks = types.Networks{}
diff --git a/internal/deployment/infra/provider/docker/provider.go b/internal/deployment/infra/provider/docker/provider.go
index 22378143..aeffbd82 100644
--- a/internal/deployment/infra/provider/docker/provider.go
+++ b/internal/deployment/infra/provider/docker/provider.go
@@ -78,13 +78,14 @@ func New(logger log.Logger, configuration ...DockerOptions) Docker {
}
// Use the given compose service and cli instead of creating new ones. Used for testing.
-func WithDockerAndCompose(cli command.Cli, composeService api.Service) DockerOptions {
+func WithTestConfig(cli command.Cli, composeService api.Service, sshConfigPath string) DockerOptions {
return func(d *docker) {
d.client = &client{
cli: cli,
api: cli.Client(),
compose: composeService,
}
+ d.sshConfig = ssh.NewFileConfigurator(sshConfigPath)
}
}
@@ -172,6 +173,14 @@ func (d *docker) Setup(ctx context.Context, target domain.Target) (domain.Target
defer client.Close()
+ if target.IsManual() {
+ return nil, client.compose.Down(ctx, targetProjectName(target.ID()), api.DownOptions{
+ RemoveOrphans: true,
+ Images: "all",
+ Volumes: true,
+ })
+ }
+
project, assigned, err := newProxyProjectBuilder(client, target).Build(ctx)
if err != nil {
@@ -224,7 +233,7 @@ func (d *docker) Expose(ctx context.Context, target domain.Target, container str
func (d *docker) Deploy(
ctx context.Context,
deploymentCtx domain.DeploymentContext,
- depl domain.Deployment,
+ deployment domain.Deployment,
target domain.Target,
registries []domain.Registry,
) (domain.Services, error) {
@@ -244,7 +253,7 @@ func (d *docker) Deploy(
logger.Infof("using custom registries: %s", strings.Join(client.registries, ", "))
}
- project, services, err := newDeploymentProjectBuilder(deploymentCtx, depl).Build(ctx)
+ project, services, err := newDeploymentProjectBuilder(deploymentCtx, deployment, target).Build(ctx)
if err != nil {
return nil, err
@@ -267,19 +276,21 @@ func (d *docker) Deploy(
return nil, ErrComposeFailed
}
- if target.Url().UseSSL() {
- logger.Infof("you may have to wait for certificates to be generated before your app is available")
- }
+ if url, isManagedBySeelf := target.Url().TryGet(); isManagedBySeelf {
+ if url.UseSSL() {
+ logger.Infof("you may have to wait for certificates to be generated before your app is available")
+ }
- if len(services.CustomEntrypoints()) > 0 {
- logger.Infof("this deployment uses custom entrypoints. If this is the first time, you may have to wait a few seconds for the target to find available ports and expose them appropriately")
+ if len(services.CustomEntrypoints()) > 0 {
+ logger.Infof("this deployment uses custom entrypoints. If this is the first time, you may have to wait a few seconds for the target to find available ports and expose them appropriately")
+ }
}
prunedCount, err := client.PruneImages(ctx, filters.NewArgs(
filters.Arg("dangling", "true"),
- filters.Arg("label", AppLabel+"="+string(depl.ID().AppID())),
+ filters.Arg("label", AppLabel+"="+string(deployment.ID().AppID())),
filters.Arg("label", TargetLabel+"="+string(target.ID())),
- filters.Arg("label", EnvironmentLabel+"="+string(depl.Config().Environment())),
+ filters.Arg("label", EnvironmentLabel+"="+string(deployment.Config().Environment())),
))
if err != nil {
diff --git a/internal/deployment/infra/provider/docker/provider_test.go b/internal/deployment/infra/provider/docker/provider_test.go
index 43e0fd2d..e5555576 100644
--- a/internal/deployment/infra/provider/docker/provider_test.go
+++ b/internal/deployment/infra/provider/docker/provider_test.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
+ "path/filepath"
"slices"
"strconv"
"strings"
@@ -12,6 +13,7 @@ import (
"github.com/YuukanOO/seelf/cmd/config"
"github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/internal/deployment/infra/artifact"
"github.com/YuukanOO/seelf/internal/deployment/infra/provider/docker"
"github.com/YuukanOO/seelf/internal/deployment/infra/source/raw"
@@ -44,7 +46,7 @@ func Test_Provider(t *testing.T) {
os.RemoveAll(opts.DataDir())
})
- return docker.New(logger, docker.WithDockerAndCompose(mock, mock)), mock
+ return docker.New(logger, docker.WithTestConfig(mock, mock, filepath.Join(opts.DataDir(), "config"))), mock
}
t.Run("should be able to prepare a docker provider config from a raw payload", func(t *testing.T) {
@@ -213,315 +215,568 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID
}
})
- t.Run("should setup a new non-ssl target without custom entrypoints", func(t *testing.T) {
- target := createTarget("http://docker.localhost")
- targetIdLower := strings.ToLower(string(target.ID()))
-
- provider, mock := arrange(config.Default(config.WithTestDefaults()))
-
- assigned, err := provider.Setup(context.Background(), target)
-
- assert.Nil(t, err)
- assert.DeepEqual(t, domain.TargetEntrypointsAssigned{}, assigned)
- assert.HasLength(t, 1, mock.ups)
- assert.DeepEqual(t, &types.Project{
- Name: "seelf-internal-" + targetIdLower,
- Services: types.Services{
- "proxy": {
- Name: "proxy",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
- },
- Image: "traefik:v2.11",
- Restart: types.RestartPolicyUnlessStopped,
- Command: types.ShellCommand{
- "--entrypoints.http.address=:80",
- "--providers.docker",
- fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
- docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
- fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
- "--providers.docker.network=seelf-gateway-" + targetIdLower,
- },
- Ports: []types.ServicePortConfig{
- {Target: 80, Published: "80"},
- },
- Volumes: []types.ServiceVolumeConfig{
- {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
- },
- CustomLabels: types.Labels{
- api.ProjectLabel: "seelf-internal-" + targetIdLower,
- api.ServiceLabel: "proxy",
- api.VersionLabel: api.ComposeVersion,
- api.ConfigFilesLabel: "",
- api.OneoffLabel: "False",
+ t.Run("should correctly setup needed stuff on a target", func(t *testing.T) {
+ t.Run("with automatic proxy, no-ssl, no custom entrypoints", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ targetIdLower := strings.ToLower(string(target.ID()))
+ provider, mock := arrange(config.Default(config.WithTestDefaults()))
+
+ assigned, err := provider.Setup(context.Background(), target)
+
+ assert.Nil(t, err)
+ assert.DeepEqual(t, domain.TargetEntrypointsAssigned{}, assigned)
+ assert.HasLength(t, 1, mock.ups)
+ assert.DeepEqual(t, &types.Project{
+ Name: "seelf-internal-" + targetIdLower,
+ Services: types.Services{
+ "proxy": {
+ Name: "proxy",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
+ Image: "traefik:v2.11",
+ Restart: types.RestartPolicyUnlessStopped,
+ Command: types.ShellCommand{
+ "--entrypoints.http.address=:80",
+ "--providers.docker",
+ fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
+ docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
+ fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
+ "--providers.docker.network=seelf-gateway-" + targetIdLower,
+ },
+ Ports: []types.ServicePortConfig{
+ {Target: 80, Published: "80"},
+ },
+ Volumes: []types.ServiceVolumeConfig{
+ {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
+ },
+ CustomLabels: types.Labels{
+ api.ProjectLabel: "seelf-internal-" + targetIdLower,
+ api.ServiceLabel: "proxy",
+ api.VersionLabel: api.ComposeVersion,
+ api.ConfigFilesLabel: "",
+ api.OneoffLabel: "False",
+ },
},
},
- },
- Networks: types.Networks{
- "default": types.NetworkConfig{
- Name: "seelf-gateway-" + targetIdLower,
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ Networks: types.Networks{
+ "default": types.NetworkConfig{
+ Name: "seelf-gateway-" + targetIdLower,
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
},
},
- },
- }, mock.ups[0].project)
- })
-
- t.Run("should setup a new ssl target without custom entrypoints", func(t *testing.T) {
- target := createTarget("https://docker.localhost")
- targetIdLower := strings.ToLower(string(target.ID()))
-
- provider, mock := arrange(config.Default(config.WithTestDefaults()))
-
- assigned, err := provider.Setup(context.Background(), target)
+ }, mock.ups[0].project)
+ })
- assert.Nil(t, err)
- assert.DeepEqual(t, domain.TargetEntrypointsAssigned{}, assigned)
- assert.HasLength(t, 1, mock.ups)
- assert.DeepEqual(t, &types.Project{
- Name: "seelf-internal-" + targetIdLower,
- Services: types.Services{
- "proxy": {
- Name: "proxy",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
- },
- Image: "traefik:v2.11",
- Restart: types.RestartPolicyUnlessStopped,
- Command: types.ShellCommand{
- fmt.Sprintf("--certificatesresolvers.%s.acme.storage=/letsencrypt/acme.json", "seelf-resolver-"+targetIdLower),
- fmt.Sprintf("--certificatesresolvers.%s.acme.tlschallenge=true", "seelf-resolver-"+targetIdLower),
- "--entrypoints.http.address=:443",
- "--entrypoints.http.http.tls.certresolver=seelf-resolver-" + targetIdLower,
- "--entrypoints.insecure.address=:80",
- "--entrypoints.insecure.http.redirections.entryPoint.scheme=https",
- "--entrypoints.insecure.http.redirections.entryPoint.to=http",
- "--providers.docker",
- fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
- docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
- fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
- "--providers.docker.network=seelf-gateway-" + targetIdLower,
- },
- Ports: []types.ServicePortConfig{
- {Target: 80, Published: "80"},
- {Target: 443, Published: "443"},
- },
- Volumes: []types.ServiceVolumeConfig{
- {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
- {Type: types.VolumeTypeVolume, Source: "letsencrypt", Target: "/letsencrypt"},
- },
- CustomLabels: types.Labels{
- api.ProjectLabel: "seelf-internal-" + targetIdLower,
- api.ServiceLabel: "proxy",
- api.VersionLabel: api.ComposeVersion,
- api.ConfigFilesLabel: "",
- api.OneoffLabel: "False",
+ t.Run("with automatic proxy, ssl, no custom entrypoints", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("https://docker.localhost")), true)))
+ targetIdLower := strings.ToLower(string(target.ID()))
+ provider, mock := arrange(config.Default(config.WithTestDefaults()))
+
+ assigned, err := provider.Setup(context.Background(), target)
+
+ assert.Nil(t, err)
+ assert.DeepEqual(t, domain.TargetEntrypointsAssigned{}, assigned)
+ assert.HasLength(t, 1, mock.ups)
+ assert.DeepEqual(t, &types.Project{
+ Name: "seelf-internal-" + targetIdLower,
+ Services: types.Services{
+ "proxy": {
+ Name: "proxy",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
+ Image: "traefik:v2.11",
+ Restart: types.RestartPolicyUnlessStopped,
+ Command: types.ShellCommand{
+ fmt.Sprintf("--certificatesresolvers.%s.acme.storage=/letsencrypt/acme.json", "seelf-resolver-"+targetIdLower),
+ fmt.Sprintf("--certificatesresolvers.%s.acme.tlschallenge=true", "seelf-resolver-"+targetIdLower),
+ "--entrypoints.http.address=:443",
+ "--entrypoints.http.http.tls.certresolver=seelf-resolver-" + targetIdLower,
+ "--entrypoints.insecure.address=:80",
+ "--entrypoints.insecure.http.redirections.entryPoint.scheme=https",
+ "--entrypoints.insecure.http.redirections.entryPoint.to=http",
+ "--providers.docker",
+ fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
+ docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
+ fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
+ "--providers.docker.network=seelf-gateway-" + targetIdLower,
+ },
+ Ports: []types.ServicePortConfig{
+ {Target: 80, Published: "80"},
+ {Target: 443, Published: "443"},
+ },
+ Volumes: []types.ServiceVolumeConfig{
+ {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
+ {Type: types.VolumeTypeVolume, Source: "letsencrypt", Target: "/letsencrypt"},
+ },
+ CustomLabels: types.Labels{
+ api.ProjectLabel: "seelf-internal-" + targetIdLower,
+ api.ServiceLabel: "proxy",
+ api.VersionLabel: api.ComposeVersion,
+ api.ConfigFilesLabel: "",
+ api.OneoffLabel: "False",
+ },
},
},
- },
- Networks: types.Networks{
- "default": types.NetworkConfig{
- Name: "seelf-gateway-" + targetIdLower,
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ Networks: types.Networks{
+ "default": types.NetworkConfig{
+ Name: "seelf-gateway-" + targetIdLower,
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
},
},
- },
- Volumes: types.Volumes{
- "letsencrypt": types.VolumeConfig{
- Name: "seelf-internal-" + targetIdLower + "_letsencrypt",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ Volumes: types.Volumes{
+ "letsencrypt": types.VolumeConfig{
+ Name: "seelf-internal-" + targetIdLower + "_letsencrypt",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
},
},
- },
- }, mock.ups[0].project)
- })
-
- t.Run("should setup a target with custom entrypoints by finding available ports", func(t *testing.T) {
- target := createTarget("http://docker.localhost")
- targetIdLower := strings.ToLower(string(target.ID()))
- depl := createDeployment(target.ID(), "")
-
- service := depl.Config().NewService("app", "")
- tcp := service.AddTCPEntrypoint(5432)
- udp := service.AddUDPEntrypoint(5433)
-
- target.ExposeEntrypoints(depl.ID().AppID(), depl.Config().Environment(), domain.Services{service})
-
- provider, mock := arrange(config.Default(config.WithTestDefaults()))
-
- assigned, err := provider.Setup(context.Background(), target)
-
- assert.Nil(t, err)
- assert.HasLength(t, 2, mock.ups)
- assert.HasLength(t, 1, mock.downs)
-
- tcpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][tcp.Name()]
- udpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][udp.Name()]
-
- assert.NotEqual(t, 0, tcpPort)
- assert.NotEqual(t, 0, udpPort)
+ }, mock.ups[0].project)
+ })
- assert.DeepEqual(t, &types.Project{
- Name: "seelf-internal-" + targetIdLower,
- Services: types.Services{
- "proxy": {
- Name: "proxy",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ t.Run("with automatic proxy, custom entrypoints", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ targetIdLower := strings.ToLower(string(target.ID()))
+ app := fixture.App(fixture.WithAppName("my-app"), fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ))
+ deployment := fixture.Deployment(fixture.FromApp(app))
+ service := deployment.Config().NewService("app", "")
+ tcp := service.AddTCPEntrypoint(5432)
+ udp := service.AddUDPEntrypoint(5433)
+ target.ExposeEntrypoints(deployment.Config().AppID(), deployment.Config().Environment(), domain.Services{service})
+ provider, mock := arrange(config.Default(config.WithTestDefaults()))
+
+ assigned, err := provider.Setup(context.Background(), target)
+
+ assert.Nil(t, err)
+ assert.HasLength(t, 2, mock.ups, "should have launch two projects since it has to find available ports")
+ assert.HasLength(t, 1, mock.downs)
+ tcpPort := assigned[deployment.ID().AppID()][deployment.Config().Environment()][tcp.Name()]
+ udpPort := assigned[deployment.ID().AppID()][deployment.Config().Environment()][udp.Name()]
+
+ assert.NotZero(t, tcpPort)
+ assert.NotZero(t, udpPort)
+
+ assert.DeepEqual(t, &types.Project{
+ Name: "seelf-internal-" + targetIdLower,
+ Services: types.Services{
+ "proxy": {
+ Name: "proxy",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
+ Image: "traefik:v2.11",
+ Restart: types.RestartPolicyUnlessStopped,
+ Command: types.ShellCommand{
+ "--entrypoints.http.address=:80",
+ fmt.Sprintf("--entrypoints.%s.address=:%d/tcp", tcp.Name(), tcpPort),
+ fmt.Sprintf("--entrypoints.%s.address=:%d/udp", udp.Name(), udpPort),
+ "--providers.docker",
+ fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
+ docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
+ fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
+ "--providers.docker.network=seelf-gateway-" + targetIdLower,
+ },
+ Ports: sortedPorts([]types.ServicePortConfig{
+ {Target: 80, Published: "80"},
+ {Target: uint32(tcpPort), Published: strconv.FormatUint(uint64(tcpPort), 10), Protocol: "tcp"},
+ {Target: uint32(udpPort), Published: strconv.FormatUint(uint64(udpPort), 10), Protocol: "udp"},
+ }),
+ Volumes: []types.ServiceVolumeConfig{
+ {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
+ },
+ CustomLabels: types.Labels{
+ api.ProjectLabel: "seelf-internal-" + targetIdLower,
+ api.ServiceLabel: "proxy",
+ api.VersionLabel: api.ComposeVersion,
+ api.ConfigFilesLabel: "",
+ api.OneoffLabel: "False",
+ },
},
- Image: "traefik:v2.11",
- Restart: types.RestartPolicyUnlessStopped,
- Command: types.ShellCommand{
- "--entrypoints.http.address=:80",
- fmt.Sprintf("--entrypoints.%s.address=:%d/tcp", tcp.Name(), tcpPort),
- fmt.Sprintf("--entrypoints.%s.address=:%d/udp", udp.Name(), udpPort),
- "--providers.docker",
- fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
- docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
- fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
- "--providers.docker.network=seelf-gateway-" + targetIdLower,
+ },
+ Networks: types.Networks{
+ "default": types.NetworkConfig{
+ Name: "seelf-gateway-" + targetIdLower,
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
},
- Ports: sortedPorts([]types.ServicePortConfig{
- {Target: 80, Published: "80"},
- {Target: uint32(tcpPort), Published: strconv.FormatUint(uint64(tcpPort), 10), Protocol: "tcp"},
- {Target: uint32(udpPort), Published: strconv.FormatUint(uint64(udpPort), 10), Protocol: "udp"},
- }),
- Volumes: []types.ServiceVolumeConfig{
- {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
+ },
+ }, mock.ups[1].project)
+ })
+
+ t.Run("with automatic proxy, custom entrypoints and already determined ports", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ targetIdLower := strings.ToLower(string(target.ID()))
+ app := fixture.App(fixture.WithAppName("my-app"), fixture.WithEnvironmentConfig(
+ domain.NewEnvironmentConfig(target.ID()),
+ domain.NewEnvironmentConfig(target.ID()),
+ ))
+ deployment := fixture.Deployment(fixture.FromApp(app))
+ service := deployment.Config().NewService("app", "")
+ tcp := service.AddTCPEntrypoint(5432)
+ udp := service.AddUDPEntrypoint(5433)
+ target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{service})
+ target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
+ deployment.ID().AppID(): {
+ deployment.Config().Environment(): {
+ tcp.Name(): 5432,
+ udp.Name(): 5433,
},
- CustomLabels: types.Labels{
- api.ProjectLabel: "seelf-internal-" + targetIdLower,
- api.ServiceLabel: "proxy",
- api.VersionLabel: api.ComposeVersion,
- api.ConfigFilesLabel: "",
- api.OneoffLabel: "False",
+ },
+ }, nil)
+ newTcp := service.AddTCPEntrypoint(5434)
+ newUdp := service.AddUDPEntrypoint(5435)
+ target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), domain.Services{service})
+ provider, mock := arrange(config.Default(config.WithTestDefaults()))
+
+ assigned, err := provider.Setup(context.Background(), target)
+
+ assert.Nil(t, err)
+ assert.HasLength(t, 2, mock.ups)
+ assert.HasLength(t, 1, mock.downs)
+ assert.Equal(t, 2, len(assigned[deployment.ID().AppID()][deployment.Config().Environment()]))
+
+ tcpPort := assigned[deployment.ID().AppID()][deployment.Config().Environment()][newTcp.Name()]
+ udpPort := assigned[deployment.ID().AppID()][deployment.Config().Environment()][newUdp.Name()]
+
+ assert.NotZero(t, tcpPort)
+ assert.NotZero(t, udpPort)
+
+ assert.DeepEqual(t, &types.Project{
+ Name: "seelf-internal-" + targetIdLower,
+ Services: types.Services{
+ "proxy": {
+ Name: "proxy",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
+ Image: "traefik:v2.11",
+ Restart: types.RestartPolicyUnlessStopped,
+ Command: types.ShellCommand{
+ "--entrypoints.http.address=:80",
+ fmt.Sprintf("--entrypoints.%s.address=:5432/tcp", tcp.Name()),
+ fmt.Sprintf("--entrypoints.%s.address=:5433/udp", udp.Name()),
+ fmt.Sprintf("--entrypoints.%s.address=:%d/tcp", newTcp.Name(), tcpPort),
+ fmt.Sprintf("--entrypoints.%s.address=:%d/udp", newUdp.Name(), udpPort),
+ "--providers.docker",
+ fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
+ docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
+ fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
+ "--providers.docker.network=seelf-gateway-" + targetIdLower,
+ },
+ Ports: sortedPorts([]types.ServicePortConfig{
+ {Target: 80, Published: "80"},
+ {Target: 5432, Published: "5432", Protocol: "tcp"},
+ {Target: 5433, Published: "5433", Protocol: "udp"},
+ {Target: uint32(tcpPort), Published: strconv.FormatUint(uint64(tcpPort), 10), Protocol: "tcp"},
+ {Target: uint32(udpPort), Published: strconv.FormatUint(uint64(udpPort), 10), Protocol: "udp"},
+ }),
+ Volumes: []types.ServiceVolumeConfig{
+ {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
+ },
+ CustomLabels: types.Labels{
+ api.ProjectLabel: "seelf-internal-" + targetIdLower,
+ api.ServiceLabel: "proxy",
+ api.VersionLabel: api.ComposeVersion,
+ api.ConfigFilesLabel: "",
+ api.OneoffLabel: "False",
+ },
},
},
- },
- Networks: types.Networks{
- "default": types.NetworkConfig{
- Name: "seelf-gateway-" + targetIdLower,
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ Networks: types.Networks{
+ "default": types.NetworkConfig{
+ Name: "seelf-gateway-" + targetIdLower,
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ },
},
},
- },
- }, mock.ups[1].project)
- })
-
- t.Run("should setup a target with custom entrypoints by using provided ports if any", func(t *testing.T) {
- target := createTarget("http://docker.localhost")
- targetIdLower := strings.ToLower(string(target.ID()))
- depl := createDeployment(target.ID(), "")
-
- service := depl.Config().NewService("app", "")
- tcp := service.AddTCPEntrypoint(5432)
- udp := service.AddUDPEntrypoint(5433)
+ }, mock.ups[1].project)
+ })
- target.ExposeEntrypoints(depl.ID().AppID(), depl.Config().Environment(), domain.Services{service})
- target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{
- depl.ID().AppID(): {
- depl.Config().Environment(): {
- tcp.Name(): 5432,
- udp.Name(): 5433,
- },
- },
- }, nil)
+ t.Run("with manual target, should not deploy the proxy and remove existing one", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ provider, mock := arrange(config.Default(config.WithTestDefaults()))
- newTcp := service.AddTCPEntrypoint(5434)
- newUdp := service.AddUDPEntrypoint(5435)
- target.ExposeEntrypoints(depl.ID().AppID(), depl.Config().Environment(), domain.Services{service})
+ assigned, err := provider.Setup(context.Background(), target)
- provider, mock := arrange(config.Default(config.WithTestDefaults()))
+ assert.Nil(t, err)
+ assert.True(t, assigned == nil)
+ assert.HasLength(t, 0, mock.ups)
+ assert.HasLength(t, 1, mock.downs)
+ assert.Equal(t, "seelf-internal-"+strings.ToLower(string(target.ID())), mock.downs[0].projectName)
+ })
+ })
- assigned, err := provider.Setup(context.Background(), target)
+ t.Run("should be able to process a deployment", func(t *testing.T) {
+ t.Run("should returns an error if no valid compose file was found", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ deployment := fixture.Deployment()
+ opts := config.Default(config.WithTestDefaults())
+ artifactManager := artifact.NewLocal(opts, logger)
+ ctx, err := artifactManager.PrepareBuild(context.Background(), deployment)
+ assert.Nil(t, err)
+ defer ctx.Logger().Close()
+ provider, _ := arrange(opts)
- assert.Nil(t, err)
- assert.HasLength(t, 2, mock.ups)
- assert.HasLength(t, 1, mock.downs)
- assert.Equal(t, 2, len(assigned[depl.ID().AppID()][depl.Config().Environment()]))
+ _, err = provider.Deploy(context.Background(), ctx, deployment, target, nil)
- tcpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][newTcp.Name()]
- udpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][newUdp.Name()]
+ assert.ErrorIs(t, docker.ErrOpenComposeFileFailed, err)
+ })
- assert.NotEqual(t, 0, tcpPort)
- assert.NotEqual(t, 0, udpPort)
+ t.Run("should correctly transform the compose file if the target is configured as automatically exposing services", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)))
+ productionConfig := domain.NewEnvironmentConfig(target.ID())
+ productionConfig.HasEnvironmentVariables(domain.ServicesEnv{
+ "app": domain.EnvVars{
+ "DSN": "postgres://prodapp:passprod@db/app?sslmode=disable",
+ },
+ "db": domain.EnvVars{
+ "POSTGRES_USER": "prodapp",
+ "POSTGRES_PASSWORD": "passprod",
+ },
+ })
+ app := fixture.App(
+ fixture.WithAppName("my-app"),
+ fixture.WithEnvironmentConfig(
+ productionConfig,
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Production),
+ fixture.WithSourceData(raw.Data(`services:
+ sidecar:
+ image: traefik/whoami
+ profiles:
+ - production
+ app:
+ restart: unless-stopped
+ build: .
+ environment:
+ - DSN=postgres://app:apppa55word@db/app?sslmode=disable
+ depends_on:
+ - db
+ ports:
+ - "8080:8080"
+ - "8081:8081/udp"
+ - "8082:8082"
+ stagingonly:
+ image: traefik/whoami
+ ports:
+ - "8888:80"
+ profiles:
+ - staging
+ db:
+ restart: unless-stopped
+ image: postgres:14-alpine
+ volumes:
+ - dbdata:/var/lib/postgresql/data
+ environment:
+ - POSTGRES_USER=app
+ - POSTGRES_PASSWORD=apppa55word
+ ports:
+ - "5432:5432/tcp"
+volumes:
+ dbdata:`)),
+ )
+ appIdLower := strings.ToLower(string(app.ID()))
+
+ // Prepare the build
+ opts := config.Default(config.WithTestDefaults())
+ artifactManager := artifact.NewLocal(opts, logger)
+ deploymentContext, err := artifactManager.PrepareBuild(context.Background(), deployment)
+ assert.Nil(t, err)
+ assert.Nil(t, raw.New().Fetch(context.Background(), deploymentContext, deployment))
+ defer deploymentContext.Logger().Close()
+ provider, mock := arrange(opts)
+
+ services, err := provider.Deploy(context.Background(), deploymentContext, deployment, target, nil)
+
+ assert.Nil(t, err)
+ assert.HasLength(t, 1, mock.ups)
+ assert.HasLength(t, 3, services)
+
+ assert.Equal(t, "app", services[0].Name())
+ assert.Equal(t, "db", services[1].Name())
+ assert.Equal(t, "sidecar", services[2].Name())
+
+ entrypoints := services.Entrypoints()
+ assert.HasLength(t, 4, entrypoints)
+ assert.Equal(t, 8080, entrypoints[0].Port())
+ assert.Equal(t, "http", entrypoints[0].Router())
+ assert.Equal(t, string(deployment.Config().AppName()), entrypoints[0].Subdomain().Get(""))
+ assert.Equal(t, 8081, entrypoints[1].Port())
+ assert.Equal(t, "udp", entrypoints[1].Router())
+ assert.Equal(t, 8082, entrypoints[2].Port())
+ assert.Equal(t, "http", entrypoints[2].Router())
+ assert.Equal(t, string(deployment.Config().AppName()), entrypoints[2].Subdomain().Get(""))
+ assert.Equal(t, 5432, entrypoints[3].Port())
+ assert.Equal(t, "tcp", entrypoints[3].Router())
+
+ project := mock.ups[0].project
+ expectedProjectName := fmt.Sprintf("%s-%s-%s", deployment.Config().AppName(), deployment.Config().Environment(), appIdLower)
+ expectedGatewayNetworkName := "seelf-gateway-" + strings.ToLower(string(target.ID()))
+ assert.Equal(t, expectedProjectName, project.Name)
+ assert.Equal(t, 3, len(project.Services))
+
+ for _, service := range project.Services {
+ switch service.Name {
+ case "sidecar":
+ assert.Equal(t, "traefik/whoami", service.Image)
+ assert.HasLength(t, 0, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{}, service.Environment)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ }, service.Labels)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ }, service.Networks)
+ case "app":
+ httpEntrypointName := string(entrypoints[0].Name())
+ udpEntrypointName := string(entrypoints[1].Name())
+ customHttpEntrypointName := string(entrypoints[2].Name())
+ dsn := deployment.Config().EnvironmentVariablesFor("app").MustGet()["DSN"]
+
+ assert.Equal(t, fmt.Sprintf("%s-%s/app:%s", deployment.Config().AppName(), appIdLower, deployment.Config().Environment()), service.Image)
+ assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ docker.SubdomainLabel: string(deployment.Config().AppName()),
+ fmt.Sprintf("traefik.http.routers.%s.entrypoints", httpEntrypointName): "http",
+ fmt.Sprintf("traefik.http.routers.%s.service", httpEntrypointName): httpEntrypointName,
+ fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", httpEntrypointName): "8080",
+ docker.CustomEntrypointsLabel: "true",
+ fmt.Sprintf("traefik.udp.routers.%s.entrypoints", udpEntrypointName): udpEntrypointName,
+ fmt.Sprintf("traefik.udp.routers.%s.service", udpEntrypointName): udpEntrypointName,
+ fmt.Sprintf("traefik.udp.services.%s.loadbalancer.server.port", udpEntrypointName): "8081",
+ fmt.Sprintf("traefik.http.routers.%s.entrypoints", customHttpEntrypointName): customHttpEntrypointName,
+ fmt.Sprintf("traefik.http.routers.%s.service", customHttpEntrypointName): customHttpEntrypointName,
+ fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", customHttpEntrypointName): "8082",
+ }, service.Labels)
+
+ assert.HasLength(t, 0, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{
+ "DSN": &dsn,
+ }, service.Environment)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ expectedGatewayNetworkName: nil,
+ }, service.Networks)
+ case "db":
+ entrypointName := string(entrypoints[3].Name())
+ postgresUser := deployment.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_USER"]
+ postgresPassword := deployment.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_PASSWORD"]
+
+ assert.Equal(t, "postgres:14-alpine", service.Image)
+ assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ fmt.Sprintf("traefik.tcp.routers.%s.rule", entrypointName): "HostSNI(`*`)",
+ docker.CustomEntrypointsLabel: "true",
+ fmt.Sprintf("traefik.tcp.routers.%s.entrypoints", entrypointName): entrypointName,
+ fmt.Sprintf("traefik.tcp.routers.%s.service", entrypointName): entrypointName,
+ fmt.Sprintf("traefik.tcp.services.%s.loadbalancer.server.port", entrypointName): "5432",
+ }, service.Labels)
+ assert.HasLength(t, 0, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{
+ "POSTGRES_USER": &postgresUser,
+ "POSTGRES_PASSWORD": &postgresPassword,
+ }, service.Environment)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ expectedGatewayNetworkName: nil,
+ }, service.Networks)
+ assert.DeepEqual(t, []types.ServiceVolumeConfig{
+ {
+ Type: types.VolumeTypeVolume,
+ Source: "dbdata",
+ Target: "/var/lib/postgresql/data",
+ Volume: &types.ServiceVolumeVolume{},
+ },
+ }, service.Volumes)
+ default:
+ t.Fatalf("unexpected service %s", service.Name)
+ }
+ }
- assert.DeepEqual(t, &types.Project{
- Name: "seelf-internal-" + targetIdLower,
- Services: types.Services{
- "proxy": {
- Name: "proxy",
+ assert.DeepEqual(t, types.Networks{
+ "default": {
+ Name: expectedProjectName + "_default",
Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
- },
- Image: "traefik:v2.11",
- Restart: types.RestartPolicyUnlessStopped,
- Command: types.ShellCommand{
- "--entrypoints.http.address=:80",
- fmt.Sprintf("--entrypoints.%s.address=:5432/tcp", tcp.Name()),
- fmt.Sprintf("--entrypoints.%s.address=:5433/udp", udp.Name()),
- fmt.Sprintf("--entrypoints.%s.address=:%d/tcp", newTcp.Name(), tcpPort),
- fmt.Sprintf("--entrypoints.%s.address=:%d/udp", newUdp.Name(), udpPort),
- "--providers.docker",
- fmt.Sprintf("--providers.docker.constraints=(Label(`%s`, `%s`) && (Label(`%s`, `true`) || LabelRegex(`%s`, `.+`))) || Label(`%s`, `true`)",
- docker.TargetLabel, target.ID(), docker.CustomEntrypointsLabel, docker.SubdomainLabel, docker.ExposedLabel),
- fmt.Sprintf("--providers.docker.defaultrule=Host(`{{ index .Labels %s}}.docker.localhost`)", fmt.Sprintf(`"%s"`, docker.SubdomainLabel)),
- "--providers.docker.network=seelf-gateway-" + targetIdLower,
- },
- Ports: sortedPorts([]types.ServicePortConfig{
- {Target: 80, Published: "80"},
- {Target: 5432, Published: "5432", Protocol: "tcp"},
- {Target: 5433, Published: "5433", Protocol: "udp"},
- {Target: uint32(tcpPort), Published: strconv.FormatUint(uint64(tcpPort), 10), Protocol: "tcp"},
- {Target: uint32(udpPort), Published: strconv.FormatUint(uint64(udpPort), 10), Protocol: "udp"},
- }),
- Volumes: []types.ServiceVolumeConfig{
- {Type: types.VolumeTypeBind, Source: "/var/run/docker.sock", Target: "/var/run/docker.sock"},
- },
- CustomLabels: types.Labels{
- api.ProjectLabel: "seelf-internal-" + targetIdLower,
- api.ServiceLabel: "proxy",
- api.VersionLabel: api.ComposeVersion,
- api.ConfigFilesLabel: "",
- api.OneoffLabel: "False",
+ docker.TargetLabel: string(target.ID()),
+ docker.AppLabel: string(deployment.Config().AppID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
},
},
- },
- Networks: types.Networks{
- "default": types.NetworkConfig{
- Name: "seelf-gateway-" + targetIdLower,
+ expectedGatewayNetworkName: {
+ Name: expectedGatewayNetworkName,
+ External: true,
+ },
+ }, project.Networks)
+ assert.DeepEqual(t, types.Volumes{
+ "dbdata": {
+ Name: expectedProjectName + "_dbdata",
Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.AppLabel: string(deployment.Config().AppID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
},
},
- },
- }, mock.ups[1].project)
- })
-
- t.Run("should returns an error if no valid compose file was found for a deployment", func(t *testing.T) {
- target := createTarget("http://docker.localhost")
- depl := createDeployment(target.ID(), "")
- opts := config.Default(config.WithTestDefaults())
- artifactManager := artifact.NewLocal(opts, logger)
-
- ctx, err := artifactManager.PrepareBuild(context.Background(), depl)
- assert.Nil(t, err)
- defer ctx.Logger().Close()
-
- provider, _ := arrange(opts)
-
- _, err = provider.Deploy(context.Background(), ctx, depl, target, nil)
-
- assert.ErrorIs(t, docker.ErrOpenComposeFileFailed, err)
- })
+ }, project.Volumes)
+
+ assert.DeepEqual(t, filters.NewArgs(
+ filters.Arg("dangling", "true"),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.AppLabel, deployment.ID().AppID())),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.TargetLabel, target.ID())),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.EnvironmentLabel, deployment.Config().Environment())),
+ ), mock.pruneFilters)
+ })
- t.Run("should expose services from a compose file", func(t *testing.T) {
- target := createTarget("http://docker.localhost")
- depl := createDeployment(target.ID(), `services:
+ t.Run("should correctly transform the compose file if the target is configured with a manual proxy", func(t *testing.T) {
+ target := fixture.Target(fixture.WithProviderConfig(docker.Data{}))
+ productionConfig := domain.NewEnvironmentConfig(target.ID())
+ productionConfig.HasEnvironmentVariables(domain.ServicesEnv{
+ "app": domain.EnvVars{
+ "DSN": "postgres://prodapp:passprod@db/app?sslmode=disable",
+ },
+ "db": domain.EnvVars{
+ "POSTGRES_USER": "prodapp",
+ "POSTGRES_PASSWORD": "passprod",
+ },
+ })
+ app := fixture.App(
+ fixture.WithAppName("my-app"),
+ fixture.WithEnvironmentConfig(
+ productionConfig,
+ domain.NewEnvironmentConfig(target.ID()),
+ ),
+ )
+ deployment := fixture.Deployment(
+ fixture.FromApp(app),
+ fixture.ForEnvironment(domain.Production),
+ fixture.WithSourceData(raw.Data(`services:
sidecar:
image: traefik/whoami
profiles:
@@ -554,197 +809,156 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID
ports:
- "5432:5432/tcp"
volumes:
- dbdata:`)
- appIdLower := strings.ToLower(string(depl.ID().AppID()))
-
- // Prepare the build
- opts := config.Default(config.WithTestDefaults())
- artifactManager := artifact.NewLocal(opts, logger)
- ctx, err := artifactManager.PrepareBuild(context.Background(), depl)
- assert.Nil(t, err)
- assert.Nil(t, raw.New().Fetch(context.Background(), ctx, depl))
- defer ctx.Logger().Close()
-
- provider, mock := arrange(opts)
-
- services, err := provider.Deploy(context.Background(), ctx, depl, target, nil)
-
- assert.Nil(t, err)
- assert.HasLength(t, 1, mock.ups)
- assert.HasLength(t, 3, services)
-
- assert.Equal(t, "app", services[0].Name())
- assert.Equal(t, "db", services[1].Name())
- assert.Equal(t, "sidecar", services[2].Name())
-
- entrypoints := services.Entrypoints()
- assert.HasLength(t, 4, entrypoints)
- assert.Equal(t, 8080, entrypoints[0].Port())
- assert.Equal(t, "http", entrypoints[0].Router())
- assert.Equal(t, string(depl.Config().AppName()), entrypoints[0].Subdomain().Get(""))
- assert.Equal(t, 8081, entrypoints[1].Port())
- assert.Equal(t, "udp", entrypoints[1].Router())
- assert.Equal(t, 8082, entrypoints[2].Port())
- assert.Equal(t, "http", entrypoints[2].Router())
- assert.Equal(t, string(depl.Config().AppName()), entrypoints[2].Subdomain().Get(""))
- assert.Equal(t, 5432, entrypoints[3].Port())
- assert.Equal(t, "tcp", entrypoints[3].Router())
-
- project := mock.ups[0].project
- expectedProjectName := fmt.Sprintf("%s-%s-%s", depl.Config().AppName(), depl.Config().Environment(), appIdLower)
- expectedGatewayNetworkName := "seelf-gateway-" + strings.ToLower(string(target.ID()))
- assert.Equal(t, expectedProjectName, project.Name)
- assert.Equal(t, 3, len(project.Services))
-
- for _, service := range project.Services {
- switch service.Name {
- case "sidecar":
- assert.Equal(t, "traefik/whoami", service.Image)
- assert.HasLength(t, 0, service.Ports)
- assert.DeepEqual(t, types.MappingWithEquals{}, service.Environment)
- assert.DeepEqual(t, types.Labels{
- docker.AppLabel: string(depl.ID().AppID()),
- docker.TargetLabel: string(target.ID()),
- docker.EnvironmentLabel: string(depl.Config().Environment()),
- }, service.Labels)
- assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
- "default": nil,
- }, service.Networks)
- case "app":
- httpEntrypointName := string(entrypoints[0].Name())
- udpEntrypointName := string(entrypoints[1].Name())
- customHttpEntrypointName := string(entrypoints[2].Name())
- dsn := depl.Config().EnvironmentVariablesFor("app").MustGet()["DSN"]
-
- assert.Equal(t, fmt.Sprintf("%s-%s/app:%s", depl.Config().AppName(), appIdLower, depl.Config().Environment()), service.Image)
- assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart)
- assert.DeepEqual(t, types.Labels{
- docker.AppLabel: string(depl.ID().AppID()),
- docker.TargetLabel: string(target.ID()),
- docker.EnvironmentLabel: string(depl.Config().Environment()),
- docker.SubdomainLabel: string(depl.Config().AppName()),
- fmt.Sprintf("traefik.http.routers.%s.entrypoints", httpEntrypointName): "http",
- fmt.Sprintf("traefik.http.routers.%s.service", httpEntrypointName): httpEntrypointName,
- fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", httpEntrypointName): "8080",
- docker.CustomEntrypointsLabel: "true",
- fmt.Sprintf("traefik.udp.routers.%s.entrypoints", udpEntrypointName): udpEntrypointName,
- fmt.Sprintf("traefik.udp.routers.%s.service", udpEntrypointName): udpEntrypointName,
- fmt.Sprintf("traefik.udp.services.%s.loadbalancer.server.port", udpEntrypointName): "8081",
- fmt.Sprintf("traefik.http.routers.%s.entrypoints", customHttpEntrypointName): customHttpEntrypointName,
- fmt.Sprintf("traefik.http.routers.%s.service", customHttpEntrypointName): customHttpEntrypointName,
- fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", customHttpEntrypointName): "8082",
- }, service.Labels)
-
- assert.HasLength(t, 0, service.Ports)
- assert.DeepEqual(t, types.MappingWithEquals{
- "DSN": &dsn,
- }, service.Environment)
- assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
- "default": nil,
- expectedGatewayNetworkName: nil,
- }, service.Networks)
- case "db":
- entrypointName := string(entrypoints[3].Name())
- postgresUser := depl.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_USER"]
- postgresPassword := depl.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_PASSWORD"]
-
- assert.Equal(t, "postgres:14-alpine", service.Image)
- assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart)
- assert.DeepEqual(t, types.Labels{
- docker.AppLabel: string(depl.ID().AppID()),
- docker.TargetLabel: string(target.ID()),
- docker.EnvironmentLabel: string(depl.Config().Environment()),
- fmt.Sprintf("traefik.tcp.routers.%s.rule", entrypointName): "HostSNI(`*`)",
- docker.CustomEntrypointsLabel: "true",
- fmt.Sprintf("traefik.tcp.routers.%s.entrypoints", entrypointName): entrypointName,
- fmt.Sprintf("traefik.tcp.routers.%s.service", entrypointName): entrypointName,
- fmt.Sprintf("traefik.tcp.services.%s.loadbalancer.server.port", entrypointName): "5432",
- }, service.Labels)
- assert.HasLength(t, 0, service.Ports)
- assert.DeepEqual(t, types.MappingWithEquals{
- "POSTGRES_USER": &postgresUser,
- "POSTGRES_PASSWORD": &postgresPassword,
- }, service.Environment)
- assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
- "default": nil,
- expectedGatewayNetworkName: nil,
- }, service.Networks)
- assert.DeepEqual(t, []types.ServiceVolumeConfig{
- {
- Type: types.VolumeTypeVolume,
- Source: "dbdata",
- Target: "/var/lib/postgresql/data",
- Volume: &types.ServiceVolumeVolume{},
- },
- }, service.Volumes)
- default:
- t.Fatalf("unexpected service %s", service.Name)
+ dbdata:`)),
+ )
+ appIdLower := strings.ToLower(string(app.ID()))
+
+ // Prepare the build
+ opts := config.Default(config.WithTestDefaults())
+ artifactManager := artifact.NewLocal(opts, logger)
+ deploymentContext, err := artifactManager.PrepareBuild(context.Background(), deployment)
+ assert.Nil(t, err)
+ assert.Nil(t, raw.New().Fetch(context.Background(), deploymentContext, deployment))
+ defer deploymentContext.Logger().Close()
+ provider, mock := arrange(opts)
+
+ services, err := provider.Deploy(context.Background(), deploymentContext, deployment, target, nil)
+
+ assert.Nil(t, err)
+ assert.HasLength(t, 1, mock.ups)
+ assert.HasLength(t, 3, services)
+
+ assert.Equal(t, "app", services[0].Name())
+ assert.Equal(t, "db", services[1].Name())
+ assert.Equal(t, "sidecar", services[2].Name())
+
+ assert.HasLength(t, 0, services.Entrypoints())
+
+ project := mock.ups[0].project
+ expectedProjectName := fmt.Sprintf("%s-%s-%s", deployment.Config().AppName(), deployment.Config().Environment(), appIdLower)
+ assert.Equal(t, expectedProjectName, project.Name)
+ assert.Equal(t, 3, len(project.Services))
+
+ for _, service := range project.Services {
+ switch service.Name {
+ case "sidecar":
+ assert.Equal(t, "traefik/whoami", service.Image)
+ assert.HasLength(t, 0, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{}, service.Environment)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ }, service.Labels)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ }, service.Networks)
+ case "app":
+ dsn := deployment.Config().EnvironmentVariablesFor("app").MustGet()["DSN"]
+
+ assert.Equal(t, fmt.Sprintf("%s-%s/app:%s", deployment.Config().AppName(), appIdLower, deployment.Config().Environment()), service.Image)
+ assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ }, service.Labels)
+
+ assert.DeepEqual(t, []types.ServicePortConfig{
+ {
+ Protocol: "tcp",
+ Mode: "ingress",
+ Target: 8080,
+ Published: "8080",
+ },
+ {
+ Protocol: "udp",
+ Mode: "ingress",
+ Target: 8081,
+ Published: "8081",
+ },
+ {
+ Protocol: "tcp",
+ Mode: "ingress",
+ Target: 8082,
+ Published: "8082",
+ },
+ }, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{
+ "DSN": &dsn,
+ }, service.Environment)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ }, service.Networks)
+ case "db":
+ postgresUser := deployment.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_USER"]
+ postgresPassword := deployment.Config().EnvironmentVariablesFor("db").MustGet()["POSTGRES_PASSWORD"]
+
+ assert.Equal(t, "postgres:14-alpine", service.Image)
+ assert.Equal(t, types.RestartPolicyUnlessStopped, service.Restart)
+ assert.DeepEqual(t, types.Labels{
+ docker.AppLabel: string(deployment.ID().AppID()),
+ docker.TargetLabel: string(target.ID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ }, service.Labels)
+ assert.DeepEqual(t, []types.ServicePortConfig{
+ {
+ Protocol: "tcp",
+ Mode: "ingress",
+ Target: 5432,
+ Published: "5432",
+ },
+ }, service.Ports)
+ assert.DeepEqual(t, types.MappingWithEquals{
+ "POSTGRES_USER": &postgresUser,
+ "POSTGRES_PASSWORD": &postgresPassword,
+ }, service.Environment)
+ assert.DeepEqual(t, map[string]*types.ServiceNetworkConfig{
+ "default": nil,
+ }, service.Networks)
+ assert.DeepEqual(t, []types.ServiceVolumeConfig{
+ {
+ Type: types.VolumeTypeVolume,
+ Source: "dbdata",
+ Target: "/var/lib/postgresql/data",
+ Volume: &types.ServiceVolumeVolume{},
+ },
+ }, service.Volumes)
+ default:
+ t.Fatalf("unexpected service %s", service.Name)
+ }
}
- }
- assert.DeepEqual(t, types.Networks{
- "default": {
- Name: expectedProjectName + "_default",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
- docker.AppLabel: string(depl.Config().AppID()),
- docker.EnvironmentLabel: string(depl.Config().Environment()),
+ assert.DeepEqual(t, types.Networks{
+ "default": {
+ Name: expectedProjectName + "_default",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ docker.AppLabel: string(deployment.Config().AppID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ },
},
- },
- expectedGatewayNetworkName: {
- Name: expectedGatewayNetworkName,
- External: true,
- },
- }, project.Networks)
- assert.DeepEqual(t, types.Volumes{
- "dbdata": {
- Name: expectedProjectName + "_dbdata",
- Labels: types.Labels{
- docker.TargetLabel: string(target.ID()),
- docker.AppLabel: string(depl.Config().AppID()),
- docker.EnvironmentLabel: string(depl.Config().Environment()),
+ }, project.Networks)
+ assert.DeepEqual(t, types.Volumes{
+ "dbdata": {
+ Name: expectedProjectName + "_dbdata",
+ Labels: types.Labels{
+ docker.TargetLabel: string(target.ID()),
+ docker.AppLabel: string(deployment.Config().AppID()),
+ docker.EnvironmentLabel: string(deployment.Config().Environment()),
+ },
},
- },
- }, project.Volumes)
-
- assert.DeepEqual(t, filters.NewArgs(
- filters.Arg("dangling", "true"),
- filters.Arg("label", fmt.Sprintf("%s=%s", docker.AppLabel, depl.ID().AppID())),
- filters.Arg("label", fmt.Sprintf("%s=%s", docker.TargetLabel, target.ID())),
- filters.Arg("label", fmt.Sprintf("%s=%s", docker.EnvironmentLabel, depl.Config().Environment())),
- ), mock.pruneFilters)
- })
-}
-
-func createTarget(url string) domain.Target {
- return must.Panic(domain.NewTarget(
- "a target",
- domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom(url)), true),
- domain.NewProviderConfigRequirement(docker.Data{}, true),
- "uid",
- ))
-}
-
-func createDeployment(target domain.TargetID, data string) domain.Deployment {
- productionConfig := domain.NewEnvironmentConfig(target)
- productionConfig.HasEnvironmentVariables(domain.ServicesEnv{
- "app": domain.EnvVars{
- "DSN": "postgres://prodapp:passprod@db/app?sslmode=disable",
- },
- "db": domain.EnvVars{
- "POSTGRES_USER": "prodapp",
- "POSTGRES_PASSWORD": "passprod",
- },
+ }, project.Volumes)
+
+ assert.DeepEqual(t, filters.NewArgs(
+ filters.Arg("dangling", "true"),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.AppLabel, deployment.ID().AppID())),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.TargetLabel, target.ID())),
+ filters.Arg("label", fmt.Sprintf("%s=%s", docker.EnvironmentLabel, deployment.Config().Environment())),
+ ), mock.pruneFilters)
+ })
})
- app := must.Panic(domain.NewApp(
- "my-app",
- domain.NewEnvironmentConfigRequirement(productionConfig, true, true),
- domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target), true, true),
- "uid",
- ))
-
- return must.Panic(app.NewDeployment(1, raw.Data(data), domain.Production, "uid"))
}
func sortedPorts(ports []types.ServicePortConfig) []types.ServicePortConfig {
@@ -845,19 +1059,3 @@ func (d *dockerMockCli) ImagesPrune(_ context.Context, criteria filters.Args) (i
d.parent.pruneFilters = criteria
return image.PruneReport{}, nil
}
-
-// func (d *dockerMockService) ContainerList(context.Context, container.ListOptions) ([]dockertypes.Container, error) {
-// return nil, nil
-// }
-
-// func (d *dockerMockService) VolumeList(context.Context, volume.ListOptions) (volume.ListResponse, error) {
-// return volume.ListResponse{}, nil
-// }
-
-// func (d *dockerMockService) NetworkList(context.Context, dockertypes.NetworkListOptions) ([]dockertypes.NetworkResource, error) {
-// return nil, nil
-// }
-
-// func (d *dockerMockService) ImageList(context.Context, image.ListOptions) ([]image.Summary, error) {
-// return nil, nil
-// }
diff --git a/internal/deployment/infra/provider/docker/proxy.go b/internal/deployment/infra/provider/docker/proxy.go
index 32e8364f..2a1321ed 100644
--- a/internal/deployment/infra/provider/docker/proxy.go
+++ b/internal/deployment/infra/provider/docker/proxy.go
@@ -17,6 +17,10 @@ const (
)
type (
+ ProxyProjectBuilder interface {
+ Build(context.Context) (*types.Project, domain.TargetEntrypointsAssigned, error)
+ }
+
// Builder used to create a compose project with everything needed to deploy
// the proxy used to expose application entrypoints.
// It will handle the assignment of new entrypoints ports if needed.
@@ -44,23 +48,24 @@ type (
}
)
-func newProxyProjectBuilder(client *client, target domain.Target) *proxyProjectBuilder {
+func newProxyProjectBuilder(client *client, target domain.Target) ProxyProjectBuilder {
id := target.ID()
- idLower := strings.ToLower(string(id))
+ idLower := domain.TargetID(strings.ToLower(string(id)))
+ url := target.Url().MustGet()
b := &proxyProjectBuilder{
client: client,
target: string(id),
- host: target.Url().Host(),
+ host: url.Host(),
entrypoints: target.CustomEntrypoints(),
assigned: make(domain.TargetEntrypointsAssigned),
- networkName: targetPublicNetworkName(target.ID()),
- projectName: "seelf-internal-" + idLower,
+ networkName: targetPublicNetworkName(idLower),
+ projectName: targetProjectName(idLower),
labels: types.Labels{TargetLabel: string(id)},
}
- if target.Url().UseSSL() {
- b.certResolverName = "seelf-resolver-" + idLower
+ if url.UseSSL() {
+ b.certResolverName = "seelf-resolver-" + string(idLower)
}
return b
@@ -291,3 +296,8 @@ func ServicePortSortFunc(a, b types.ServicePortConfig) int {
func targetPublicNetworkName(id domain.TargetID) string {
return "seelf-gateway-" + strings.ToLower(string(id))
}
+
+// Retrieve the project name of a specific target
+func targetProjectName(id domain.TargetID) string {
+ return "seelf-internal-" + strings.ToLower(string(id))
+}
diff --git a/internal/deployment/infra/provider/facade.go b/internal/deployment/infra/provider/facade.go
index d9b91d8b..8df1501b 100644
--- a/internal/deployment/infra/provider/facade.go
+++ b/internal/deployment/infra/provider/facade.go
@@ -33,14 +33,14 @@ func (f *facade) Prepare(ctx context.Context, payload any, existing ...domain.Pr
return nil, domain.ErrNoValidProviderFound
}
-func (f *facade) Deploy(ctx context.Context, info domain.DeploymentContext, depl domain.Deployment, target domain.Target, registries []domain.Registry) (domain.Services, error) {
+func (f *facade) Deploy(ctx context.Context, info domain.DeploymentContext, deployment domain.Deployment, target domain.Target, registries []domain.Registry) (domain.Services, error) {
provider, err := f.providerForTarget(target)
if err != nil {
return nil, err
}
- return provider.Deploy(ctx, info, depl, target, registries)
+ return provider.Deploy(ctx, info, deployment, target, registries)
}
func (f *facade) Setup(ctx context.Context, target domain.Target) (domain.TargetEntrypointsAssigned, error) {
diff --git a/internal/deployment/infra/provider/facade_test.go b/internal/deployment/infra/provider/facade_test.go
index 12efd61f..014bc92e 100644
--- a/internal/deployment/infra/provider/facade_test.go
+++ b/internal/deployment/infra/provider/facade_test.go
@@ -5,19 +5,12 @@ import (
"testing"
"github.com/YuukanOO/seelf/internal/deployment/domain"
+ "github.com/YuukanOO/seelf/internal/deployment/fixture"
"github.com/YuukanOO/seelf/internal/deployment/infra/provider"
"github.com/YuukanOO/seelf/pkg/assert"
- "github.com/YuukanOO/seelf/pkg/must"
)
func Test_Facade(t *testing.T) {
- env := domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true)
- app := must.Panic(domain.NewApp("app", env, env, "uid"))
- depl := must.Panic(app.NewDeployment(1, dummySourceData{}, domain.Production, "uid"))
- url := domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true)
- providerConfig := domain.NewProviderConfigRequirement(dummyProviderConfig{}, true)
- target := must.Panic(domain.NewTarget("target", url, providerConfig, "uid"))
-
t.Run("should return an error if no provider can handle the payload", func(t *testing.T) {
sut := provider.NewFacade()
@@ -28,6 +21,8 @@ func Test_Facade(t *testing.T) {
t.Run("should return an error if no provider can handle the deployment", func(t *testing.T) {
sut := provider.NewFacade()
+ target := fixture.Target()
+ depl := fixture.Deployment()
_, err := sut.Deploy(context.Background(), domain.DeploymentContext{}, depl, target, nil)
@@ -36,6 +31,7 @@ func Test_Facade(t *testing.T) {
t.Run("should return an error if no provider can configure the target", func(t *testing.T) {
sut := provider.NewFacade()
+ target := fixture.Target()
_, err := sut.Setup(context.Background(), target)
@@ -44,6 +40,7 @@ func Test_Facade(t *testing.T) {
t.Run("should return an error if no provider can unconfigure the target", func(t *testing.T) {
sut := provider.NewFacade()
+ target := fixture.Target()
err := sut.RemoveConfiguration(context.Background(), target)
@@ -52,6 +49,7 @@ func Test_Facade(t *testing.T) {
t.Run("should return an error if no provider can cleanup the target", func(t *testing.T) {
sut := provider.NewFacade()
+ target := fixture.Target()
err := sut.CleanupTarget(context.Background(), target, domain.CleanupStrategyDefault)
@@ -60,23 +58,11 @@ func Test_Facade(t *testing.T) {
t.Run("should return an error if no provider can cleanup the app", func(t *testing.T) {
sut := provider.NewFacade()
+ app := fixture.App()
+ target := fixture.Target()
err := sut.Cleanup(context.Background(), app.ID(), target, domain.Production, domain.CleanupStrategyDefault)
assert.ErrorIs(t, domain.ErrNoValidProviderFound, err)
})
}
-
-type (
- dummyProviderConfig struct {
- domain.ProviderConfig
- }
-
- dummySourceData struct {
- domain.SourceData
- }
-)
-
-func (d dummyProviderConfig) Kind() string { return "dummy" }
-func (d dummySourceData) Kind() string { return "dummy" }
-func (d dummySourceData) NeedVersionControl() bool { return false }
diff --git a/internal/deployment/infra/sqlite/migrations/1706004450_add_target.up.sql b/internal/deployment/infra/sqlite/migrations/1706004450_add_target.up.sql
index 924c88e6..2f588c5b 100644
--- a/internal/deployment/infra/sqlite/migrations/1706004450_add_target.up.sql
+++ b/internal/deployment/infra/sqlite/migrations/1706004450_add_target.up.sql
@@ -73,6 +73,7 @@ SELECT
FROM targets;
-- Rename the old apps table since it will be recreated with proper NOT NULL columns
+-- I should have used a temporary table
ALTER TABLE apps RENAME TO tmp_apps;
-- Create the new apps table with proper columns
@@ -146,6 +147,7 @@ SELECT
FROM tmp_apps;
-- Do the same for deployments
+-- I should have used a temporary table
ALTER TABLE deployments RENAME TO tmp_deployments;
CREATE TABLE deployments (
diff --git a/internal/deployment/infra/sqlite/migrations/1726473707_target_url_optional.up.sql b/internal/deployment/infra/sqlite/migrations/1726473707_target_url_optional.up.sql
new file mode 100644
index 00000000..629a32f9
--- /dev/null
+++ b/internal/deployment/infra/sqlite/migrations/1726473707_target_url_optional.up.sql
@@ -0,0 +1,55 @@
+-- since we cannot change the url nullable property easily, we have to do this steps
+CREATE TEMPORARY TABLE tmp_targets AS
+SELECT *
+FROM targets;
+
+CREATE TEMPORARY TABLE tmp_apps AS
+SELECT *
+FROM apps;
+
+CREATE TEMPORARY TABLE tmp_deployments AS
+SELECT *
+FROM deployments;
+
+DELETE FROM deployments;
+DELETE FROM apps;
+DROP TABLE targets;
+
+CREATE TABLE targets (
+ id TEXT NOT NULL,
+ name TEXT NOT NULL,
+ url TEXT NULL,
+ provider_kind TEXT NOT NULL,
+ provider_fingerprint TEXT NOT NULL,
+ provider TEXT NOT NULL,
+ state_status INTEGER NOT NULL,
+ state_version DATETIME NOT NULL,
+ state_errcode TEXT NULL,
+ state_last_ready_version DATETIME NULL,
+ cleanup_requested_at DATETIME NULL,
+ cleanup_requested_by TEXT NULL,
+ created_at DATETIME NOT NULL,
+ created_by TEXT NOT NULL,
+ entrypoints TEXT NOT NULL DEFAULT '{}',
+
+ CONSTRAINT pk_targets PRIMARY KEY(id),
+ CONSTRAINT fk_targets_created_by FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE CASCADE,
+ CONSTRAINT unique_targets_url UNIQUE(url), -- unique url among all targets
+ CONSTRAINT unique_targets_provider_fingerprint UNIQUE(provider_fingerprint) -- unique provider fingerprint
+);
+
+INSERT INTO targets
+SELECT *
+FROM tmp_targets;
+
+INSERT INTO apps
+SELECT *
+FROM tmp_apps;
+
+INSERT INTO deployments
+SELECT *
+FROM tmp_deployments;
+
+DROP TABLE tmp_targets;
+DROP TABLE tmp_apps;
+DROP TABLE tmp_deployments;
\ No newline at end of file
diff --git a/internal/deployment/infra/sqlite/targets.go b/internal/deployment/infra/sqlite/targets.go
index b3b01afc..5eee1ae2 100644
--- a/internal/deployment/infra/sqlite/targets.go
+++ b/internal/deployment/infra/sqlite/targets.go
@@ -99,7 +99,6 @@ func (s *targetsStore) Write(c context.Context, targets ...*domain.Target) error
Insert("targets", builder.Values{
"id": evt.ID,
"name": evt.Name,
- "url": evt.Url,
"provider_kind": evt.Provider.Kind(),
"provider_fingerprint": evt.Provider.Fingerprint(),
"provider": evt.Provider,
@@ -136,6 +135,13 @@ func (s *targetsStore) Write(c context.Context, targets ...*domain.Target) error
}).
F("WHERE id = ?", evt.ID).
Exec(s.db, ctx)
+ case domain.TargetUrlRemoved:
+ return builder.
+ Update("targets", builder.Values{
+ "url": nil,
+ }).
+ F("WHERE id = ?", evt.ID).
+ Exec(s.db, ctx)
case domain.TargetProviderChanged:
return builder.
Update("targets", builder.Values{
diff --git a/pkg/storage/sqlite/builder/builder.go b/pkg/storage/sqlite/builder/builder.go
index b7bf74c4..1d291ef9 100644
--- a/pkg/storage/sqlite/builder/builder.go
+++ b/pkg/storage/sqlite/builder/builder.go
@@ -161,7 +161,7 @@ func (q *queryBuilder[T]) All(
defer rows.Close()
- var results []T
+ results := make([]T, 0)
// Instantiates needed stuff for data loaders
mappings := make([]keysMapping, len(loaders))