From c7d3f41588d2412c23f6dc105d405a97b82a40a2 Mon Sep 17 00:00:00 2001 From: Julien LEICHER Date: Mon, 14 Oct 2024 07:32:49 +0200 Subject: [PATCH] Manual proxy configuration & some refactors (#81) Closes #58 Also add test domain fixtures to ease testing (see #63). --- .dockerignore | 1 - .github/workflows/ci.yml | 2 +- .releaserc | 11 +- Dockerfile | 4 +- Makefile | 14 +- cmd/config/configuration.go | 9 +- .../front/src/components/target-card.svelte | 3 +- cmd/serve/front/src/lib/localization/en.ts | 10 +- cmd/serve/front/src/lib/localization/fr.ts | 9 +- cmd/serve/front/src/lib/resources/apps.ts | 2 +- cmd/serve/front/src/lib/resources/targets.ts | 6 +- .../src/routes/(main)/apps/app-form.svelte | 47 +- .../src/routes/(main)/apps/service-url.svelte | 15 + .../(main)/targets/[id]/edit/+page.svelte | 2 +- .../routes/(main)/targets/target-form.svelte | 21 +- cmd/serve/healthcheck.go | 2 +- cmd/serve/middlewares.go | 10 +- cmd/serve/server.go | 13 +- cmd/startup/server.go | 19 +- cmd/version/version.go | 2 +- docs/contributing/backend.md | 3 +- docs/guide/migration.md | 2 +- docs/guide/updating.md | 6 +- docs/reference/providers/docker.md | 2 +- docs/reference/targets.md | 11 +- go.mod | 130 +- go.sum | 345 ++--- .../create_first_account_test.go | 82 +- internal/auth/app/login/login_test.go | 75 +- .../refresh_api_key/refresh_api_key_test.go | 42 +- .../auth/app/update_user/update_user_test.go | 103 +- internal/auth/domain/context_test.go | 11 +- internal/auth/domain/email_test.go | 12 +- internal/auth/domain/user_test.go | 82 +- internal/auth/fixture/database.go | 88 ++ internal/auth/fixture/database_test.go | 30 + internal/auth/fixture/user.go | 61 + internal/auth/fixture/user_test.go | 47 + .../infra/crypto/api_key_generator_test.go | 7 +- .../auth/infra/crypto/bcrypt_hasher_test.go | 14 +- internal/auth/infra/memory/users.go | 126 -- .../app/cleanup_app/cleanup_app_test.go | 231 ++- .../cleanup_app/on_app_cleanup_requested.go | 6 +- .../app/cleanup_target/cleanup_target_test.go | 130 +- .../configure_target/configure_target_test.go | 108 +- .../on_app_cleanup_requested.go | 44 +- .../on_deployment_state_changed.go | 2 +- .../deployment/app/create_app/create_app.go | 2 +- .../app/create_app/create_app_test.go | 125 +- .../create_registry/create_registry_test.go | 79 +- .../app/create_target/create_target.go | 30 +- .../app/create_target/create_target_test.go | 148 +- .../app/delete_app/delete_app_test.go | 105 +- .../delete_registry/delete_registry_test.go | 42 +- .../app/delete_target/delete_target_test.go | 67 +- internal/deployment/app/deploy/deploy_test.go | 300 ++-- .../expose_seelf_container.go | 5 +- .../on_app_cleanup_requested.go | 2 +- .../on_app_env_changed.go | 2 +- .../on_target_delete_requested.go | 2 +- .../app/get_deployment/get_deployment_test.go | 10 +- .../deployment/app/get_target/get_target.go | 2 +- internal/deployment/app/group.go | 2 +- .../deployment/app/promote/promote_test.go | 129 +- internal/deployment/app/query.go | 6 +- .../queue_deployment/queue_deployment_test.go | 130 +- .../reconfigure_target_test.go | 69 +- .../app/redeploy/on_app_env_changed.go | 4 +- .../deployment/app/redeploy/redeploy_test.go | 100 +- .../request_app_cleanup_test.go | 64 +- .../request_target_cleanup_test.go | 104 +- .../app/update_app/update_app_test.go | 496 +++--- .../update_registry/update_registry_test.go | 195 ++- .../app/update_target/update_target.go | 14 +- .../app/update_target/update_target_test.go | 167 ++- internal/deployment/domain/app.go | 23 +- internal/deployment/domain/app_test.go | 260 ++-- internal/deployment/domain/appname_test.go | 12 +- ...eployment_config.go => config_snapshot.go} | 38 +- .../deployment/domain/config_snapshot_test.go | 66 + .../deployment/domain/credentials_test.go | 14 +- internal/deployment/domain/deployment.go | 100 +- .../domain/deployment_config_test.go | 90 -- .../deployment/domain/deployment_id_test.go | 6 +- internal/deployment/domain/deployment_test.go | 325 ++-- internal/deployment/domain/environment.go | 8 + .../deployment/domain/environment_test.go | 40 +- internal/deployment/domain/registry_test.go | 109 +- internal/deployment/domain/service.go | 181 ++- internal/deployment/domain/service_test.go | 282 ++-- internal/deployment/domain/state.go | 142 -- internal/deployment/domain/state_test.go | 169 --- internal/deployment/domain/target.go | 136 +- internal/deployment/domain/target_test.go | 1330 ++++++++++------- internal/deployment/domain/url_test.go | 58 +- .../deployment/domain/version_control_test.go | 99 +- internal/deployment/fixture/app.go | 65 + internal/deployment/fixture/app_test.go | 50 + internal/deployment/fixture/database.go | 142 ++ internal/deployment/fixture/database_test.go | 53 + internal/deployment/fixture/deployment.go | 97 ++ .../deployment/fixture/deployment_test.go | 63 + internal/deployment/fixture/registry.go | 52 + internal/deployment/fixture/registry_test.go | 40 + internal/deployment/fixture/target.go | 119 ++ internal/deployment/fixture/target_test.go | 76 + .../infra/artifact/local_artifact_manager.go | 16 +- .../artifact/local_artifact_manager_test.go | 14 +- internal/deployment/infra/artifact/logger.go | 2 +- internal/deployment/infra/memory/apps.go | 203 --- .../deployment/infra/memory/deployments.go | 162 -- .../deployment/infra/memory/registries.go | 110 -- internal/deployment/infra/memory/targets.go | 133 -- internal/deployment/infra/mod.go | 2 +- .../infra/provider/docker/client.go | 4 +- .../infra/provider/docker/data_test.go | 4 +- .../infra/provider/docker/deployment.go | 96 +- .../infra/provider/docker/provider.go | 31 +- .../infra/provider/docker/provider_test.go | 1151 ++++++++------ .../deployment/infra/provider/docker/proxy.go | 24 +- internal/deployment/infra/provider/facade.go | 4 +- .../deployment/infra/provider/facade_test.go | 44 +- internal/deployment/infra/source/git/data.go | 2 +- .../deployment/infra/sqlite/deployments.go | 2 +- .../migrations/1706004450_add_target.up.sql | 2 + .../1726473707_target_url_optional.up.sql | 55 + internal/deployment/infra/sqlite/targets.go | 8 +- pkg/apperr/error_test.go | 32 +- pkg/assert/assert.go | 220 +++ pkg/assert/assert_test.go | 589 ++++++++ pkg/bus/dispatcher.go | 14 +- pkg/bus/memory/dispatcher.go | 13 +- pkg/bus/memory/dispatcher_test.go | 32 +- pkg/bus/message_test.go | 8 +- pkg/bus/scheduler.go | 2 - pkg/bus/scheduler_test.go | 32 +- pkg/bus/spy/dispatcher.go | 48 + pkg/bus/sqlite/store.go | 28 +- pkg/config/loader_test.go | 22 +- pkg/crypto/random_test.go | 6 +- pkg/domain/action_test.go | 10 +- pkg/domain/interval_test.go | 10 +- pkg/event/event_test.go | 8 +- pkg/flag/flag_test.go | 16 +- pkg/id/id_test.go | 8 +- pkg/monad/maybe_test.go | 88 +- pkg/monad/patch_test.go | 34 +- pkg/must/panic_test.go | 8 +- pkg/ostools/file.go | 12 +- pkg/ssh/config_test.go | 72 +- pkg/ssh/host_test.go | 10 +- pkg/ssh/private_key_test.go | 8 +- pkg/storage/discriminated.go | 2 + pkg/storage/discriminated_test.go | 12 +- pkg/storage/secret_string_test.go | 14 +- pkg/storage/sqlite/builder/builder_test.go | 10 +- pkg/testutil/assertion.go | 130 -- pkg/testutil/assertion_test.go | 432 ------ pkg/types/is_test.go | 6 +- pkg/validate/numbers/numbers_test.go | 10 +- pkg/validate/strings/strings_test.go | 26 +- pkg/validate/validate_test.go | 94 +- 162 files changed, 7341 insertions(+), 5442 deletions(-) create mode 100644 cmd/serve/front/src/routes/(main)/apps/service-url.svelte create mode 100644 internal/auth/fixture/database.go create mode 100644 internal/auth/fixture/database_test.go create mode 100644 internal/auth/fixture/user.go create mode 100644 internal/auth/fixture/user_test.go delete mode 100644 internal/auth/infra/memory/users.go rename internal/deployment/domain/{deployment_config.go => config_snapshot.go} (64%) create mode 100644 internal/deployment/domain/config_snapshot_test.go delete mode 100644 internal/deployment/domain/deployment_config_test.go delete mode 100644 internal/deployment/domain/state.go delete mode 100644 internal/deployment/domain/state_test.go create mode 100644 internal/deployment/fixture/app.go create mode 100644 internal/deployment/fixture/app_test.go create mode 100644 internal/deployment/fixture/database.go create mode 100644 internal/deployment/fixture/database_test.go create mode 100644 internal/deployment/fixture/deployment.go create mode 100644 internal/deployment/fixture/deployment_test.go create mode 100644 internal/deployment/fixture/registry.go create mode 100644 internal/deployment/fixture/registry_test.go create mode 100644 internal/deployment/fixture/target.go create mode 100644 internal/deployment/fixture/target_test.go delete mode 100644 internal/deployment/infra/memory/apps.go delete mode 100644 internal/deployment/infra/memory/deployments.go delete mode 100644 internal/deployment/infra/memory/registries.go delete mode 100644 internal/deployment/infra/memory/targets.go create mode 100644 internal/deployment/infra/sqlite/migrations/1726473707_target_url_optional.up.sql create mode 100644 pkg/assert/assert.go create mode 100644 pkg/assert/assert_test.go create mode 100644 pkg/bus/spy/dispatcher.go delete mode 100644 pkg/testutil/assertion.go delete mode 100644 pkg/testutil/assertion_test.go diff --git a/.dockerignore b/.dockerignore index 33ee2a23..99c4d3ca 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,4 @@ examples -Makefile Dockerfile compose.yml docs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0088c08e..3aef1e04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,7 +101,7 @@ jobs: release: name: Release - if: github.ref_name == 'main' + if: github.ref_name == 'main' || github.ref_name == 'next' runs-on: ubuntu-latest needs: [backend, frontend] steps: diff --git a/.releaserc b/.releaserc index 2e7ce3d8..8067229b 100644 --- a/.releaserc +++ b/.releaserc @@ -1,6 +1,13 @@ { "branches": [ - "main" + { + "name": "main" + }, + { + "name": "next", + "channel": "next", + "prerelease": true + } ], "plugins": [ "@semantic-release/commit-analyzer", @@ -8,7 +15,7 @@ [ "@semantic-release/exec", { - "prepareCmd": "SEELF_VERSION=${nextRelease.version} make prepare-release && echo \"docker_tags=yuukanoo/seelf:${nextRelease.version},yuukanoo/seelf:latest\" >> \"$GITHUB_ENV\"", + "prepareCmd": "SEELF_VERSION=${nextRelease.version} make prepare-release && sh -c \"if [ ${branch.name} = 'next' ]; then echo 'docker_tags=yuukanoo/seelf:next'; else echo 'docker_tags=yuukanoo/seelf:${nextRelease.version},yuukanoo/seelf:latest'; fi\" >> \"$GITHUB_ENV\"", "publishCmd": "echo \"docker_publish=true\" >> \"$GITHUB_ENV\"" } ], diff --git a/Dockerfile b/Dockerfile index 4ca2580d..9fc9d3d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ RUN npm ci COPY ./cmd/serve/front . RUN npm run build -FROM golang:1.21-alpine AS builder +FROM golang:1.23-alpine AS builder # build-base needed to compile the sqlite3 dependency RUN apk add --update-cache build-base WORKDIR /app @@ -13,7 +13,7 @@ COPY go.* ./ RUN go mod download COPY . . COPY --from=front_builder /app/build ./cmd/serve/front/build -RUN go build -ldflags="-s -w" -o seelf +RUN make build-back FROM alpine:3.16 LABEL org.opencontainers.image.authors="julien@leicher.me" \ diff --git a/Makefile b/Makefile index 47c61290..0d13b734 100644 --- a/Makefile +++ b/Makefile @@ -7,20 +7,28 @@ serve-docs: # Launch the docs dev server serve-back: # Launch the backend API and creates an admin user if needed ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=admin LOG_LEVEL=debug go run main.go serve -test: # Launch every tests +test-front: # Launch the frontend tests cd cmd/serve/front && npm i && npm test && cd ../../.. + +test-back: # Launch the backend tests go vet ./... go test ./... --cover +test: test-front test-back # Launch every tests + ts: # Print the current timestamp, useful for migrations @date +%s outdated: # Print direct dependencies and their latest version go list -v -u -m -f '{{if not .Indirect}}{{.}}{{end}}' all -build: # Build the final binary for the current platform +build-front: # Build the frontend cd cmd/serve/front && npm i && npm run build && cd ../../.. - go build -ldflags="-s -w" -o seelf + +build-back: # Build the backend + go build -tags release -ldflags="-s -w" -o seelf + +build: build-front build-back # Build the final binary for the current platform build-docs: # Build the docs npm i && npm run docs:build diff --git a/cmd/config/configuration.go b/cmd/config/configuration.go index a50b6bc9..f60a1aeb 100644 --- a/cmd/config/configuration.go +++ b/cmd/config/configuration.go @@ -16,6 +16,7 @@ import ( "github.com/YuukanOO/seelf/pkg/log" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/must" + "github.com/YuukanOO/seelf/pkg/ostools" "github.com/YuukanOO/seelf/pkg/validate" "github.com/YuukanOO/seelf/pkg/validate/numbers" ) @@ -43,7 +44,7 @@ const ( type ( // Configuration used to configure seelf commands. Configuration interface { - serve.Options // The configuration should provide every settings needed by the seelf server + serve.Options Initialize(log.ConfigurableLogger, string) error // Initialize the configuration by loading it (from config file, env vars, etc.) } @@ -139,6 +140,11 @@ func (c *configuration) Initialize(logger log.ConfigurableLogger, path string) e return err } + // Make sure the data path exists + if err = ostools.MkdirAll(c.Data.Path); err != nil { + return err + } + // Update logger based on loaded configuration if err = logger.Configure(c.logFormat, c.logLevel); err != nil { return err @@ -173,6 +179,7 @@ func (c *configuration) Secret() []byte { return []by func (c *configuration) RunnersPollInterval() time.Duration { return c.pollInterval } func (c *configuration) RunnersDeploymentCount() int { return c.Runners.Deployment } func (c *configuration) RunnersCleanupCount() int { return c.Runners.Cleanup } +func (c *configuration) IsDebug() bool { return c.logLevel == log.DebugLevel } func (c *configuration) IsSecure() bool { // If secure has been explicitly isSet, returns it diff --git a/cmd/serve/front/src/components/target-card.svelte b/cmd/serve/front/src/components/target-card.svelte index 883b6c5a..a62fc73f 100644 --- a/cmd/serve/front/src/components/target-card.svelte +++ b/cmd/serve/front/src/components/target-card.svelte @@ -5,6 +5,7 @@ import Stack from '$components/stack.svelte'; import CleanupNotice from '$components/cleanup-notice.svelte'; import routes from '$lib/path'; + import l from '$lib/localization'; import { type Target, TargetStatus } from '$lib/resources/targets'; export let data: Target; @@ -27,7 +28,7 @@

{data.name}

-
{data.url}
+
{data.url ?? l.translate('target.manual_proxy')}
diff --git a/cmd/serve/front/src/lib/localization/en.ts b/cmd/serve/front/src/lib/localization/en.ts index e890a111..8ee82f53 100644 --- a/cmd/serve/front/src/lib/localization/en.ts +++ b/cmd/serve/front/src/lib/localization/en.ts @@ -7,8 +7,7 @@ const translations = { 'auth.signin.description': 'Please fill the form below to access your dashboard.', // App 'app.no_targets': 'No targets found', - 'app.no_targets.description': - 'You need at least one target to deploy your application. Head to the create target page to create one.', + 'app.no_targets.description': `You need at least one target to deploy your application. Head to the create target page to create one.`, 'app.not_found': "Looks like the application you're looking for does not exist. Head back to the", 'app.not_found.cta': 'homepage', 'app.blankslate': `Looks like you have no application yet.
Applications represents services you want to deploy on your infrastructure. Start by creating one!`, @@ -43,8 +42,8 @@ This action is IRREVERSIBLE and will DELETE ALL DATA associated with this applic 'app.environment.staging': 'Staging settings', 'app.environment.target': 'Deploy target', 'app.environment.target.changed': 'Target changed', - 'app.environment.target.changed.description': (url: string) => - `If you change the target, resources related to this application deployed by seelf on ${url} will be REMOVED and a new deployment on the new target will be queued if possible. If you want to backup something, do it before updating the target.`, + 'app.environment.target.changed.description': (name: string) => + `If you change the target, resources related to this application deployed by seelf on ${name} will be REMOVED and a new deployment on the new target will be queued if possible. If you want to backup something, do it before updating the target.`, 'app.environment.vars': 'Environment variables', 'app.environment.vars.service.add': 'Add service variables', 'app.environment.vars.service.delete': 'Remove service variables', @@ -77,6 +76,9 @@ This action is IRREVERSIBLE and will DELETE ALL DATA associated with this applic 'target.blankslate': `Looks like you have no target yet.
Targets determine on which host your applications will be deployed and which provider should be used. Start by creating one!`, 'target.general': 'General settings', 'target.name.help': 'The name is being used only for display, it can be anything you want.', + 'target.manual_proxy': 'Manual proxy', + 'target.automatic_proxy_configuration': 'Expose services automatically', + 'target.automatic_proxy_configuration.help': `If enabled, a proxy will be deployed on the target and your services will be automatically exposed. If disabled, you will have to manually expose your services with your preferred solution. Updating this setting may require you to redeploy your applications.`, 'target.url.help': 'All applications deployed on this target will be available as a subdomain on this root URL (without path). It should be unique among targets. You MUST configure a wildcard DNS for subdomains such as *.<url above> redirects to this target IP.', 'target.provider': 'Provider', diff --git a/cmd/serve/front/src/lib/localization/fr.ts b/cmd/serve/front/src/lib/localization/fr.ts index 85058e3d..1bb645ea 100644 --- a/cmd/serve/front/src/lib/localization/fr.ts +++ b/cmd/serve/front/src/lib/localization/fr.ts @@ -11,7 +11,7 @@ export default { 'Remplissez le formulaire ci-dessous pour accéder au tableau de bord.', // App 'app.no_targets': 'Aucune cible trouvée', - 'app.no_targets.description': `Vous avez besoin d'au moins une cible pour pouvoir déployer votre application. Dirigez-vous vers la page de création pour en créer une.`, + 'app.no_targets.description': `Vous avez besoin d'au moins une cible pour pouvoir déployer votre application. Dirigez-vous vers la page de création pour en créer une.`, 'app.not_found': "Il semblerait que l'application que vous recherchez n'existe pas. Retournez à la", 'app.not_found.cta': "page d'accueil", @@ -47,8 +47,8 @@ Cette action est IRRÉVERSIBLE et supprimera TOUTES LES DONNÉES associées sur 'app.environment.staging': 'Paramètres de staging', 'app.environment.target': 'Cible de déploiement', 'app.environment.target.changed': 'Cible mise à jour', - 'app.environment.target.changed.description': (url: string) => - `Si vous changez de cible, toutes les ressources liées à cette application déployées par seelf sur ${url} seront SUPPRIMÉES et un déploiement sur la nouvelle cible sera programmé si possible. Si vous devez sauvegarder quelque chose, faites le avant de changer la cible.`, + 'app.environment.target.changed.description': (name: string) => + `Si vous changez de cible, toutes les ressources liées à cette application déployées par seelf sur ${name} seront SUPPRIMÉES et un déploiement sur la nouvelle cible sera programmé si possible. Si vous devez sauvegarder quelque chose, faites le avant de changer la cible.`, 'app.environment.vars': "Variables d'environnement", 'app.environment.vars.service.add': 'Ajouter un service', 'app.environment.vars.service.delete': 'Supprimer le service', @@ -83,6 +83,9 @@ Cette action est IRRÉVERSIBLE et supprimera TOUTES LES DONNÉES associées sur 'target.blankslate': `Aucune cible pour le moment.
Les cibles déterminent sur quel hôte vos applications seront déployées. Commencez par en créer une !`, 'target.general': 'Paramètres généraux', 'target.name.help': `Le nom est utilisé uniquement pour l'affichage. Vous pouvez choisir ce que vous voulez.`, + 'target.manual_proxy': 'Proxy manuel', + 'target.automatic_proxy_configuration': 'Exposer les services automatiquement', + 'target.automatic_proxy_configuration.help': `Si activé, un proxy sera déployé sur la cible et vos services seront automatiquement exposés. Si désactivé, vous devrez faire le nécessaire pour rendre vos services accessibles en utilisant la méthode de votre choix. Changer ce paramètre pourra nécessiter le redéploiement de vos applications.`, 'target.url.help': `Toutes les applications déployées sur cette cible seront disponibles en tant que sous-domaine de cette URL racine (sans sous-chemin). Elle doit être unique parmi les cibles. Vous DEVEZ configurer un DNS wildcard pour les sous-domaines de telle sorte que *.<url configurée> redirige vers l'IP de cette cible.`, 'target.provider': 'Fournisseur', 'target.provider.docker.help': "Docker engine DOIT être installé sur l'hôte.", diff --git a/cmd/serve/front/src/lib/resources/apps.ts b/cmd/serve/front/src/lib/resources/apps.ts index 04a37fb4..cdc213e1 100644 --- a/cmd/serve/front/src/lib/resources/apps.ts +++ b/cmd/serve/front/src/lib/resources/apps.ts @@ -25,7 +25,7 @@ export type VersionControl = { url: string; token?: string }; export type TargetSummary = { id: string; name: string; - url: string; + url?: string; }; export type AppDetail = { diff --git a/cmd/serve/front/src/lib/resources/targets.ts b/cmd/serve/front/src/lib/resources/targets.ts index d65e0398..3ef4402a 100644 --- a/cmd/serve/front/src/lib/resources/targets.ts +++ b/cmd/serve/front/src/lib/resources/targets.ts @@ -29,7 +29,7 @@ export type ProviderTypes = ProviderConfigData['kind']; export type Target = { id: string; name: string; - url: string; + url?: string; provider: ProviderConfigData; state: TargetState; cleanup_requested_at?: string; @@ -39,7 +39,7 @@ export type Target = { export type CreateTarget = { name: string; - url: string; + url?: string; docker?: { host?: string; user?: string; @@ -50,7 +50,7 @@ export type CreateTarget = { export type UpdateTarget = { name?: string; - url?: string; + url: Patch; docker?: { host?: string; user?: string; diff --git a/cmd/serve/front/src/routes/(main)/apps/app-form.svelte b/cmd/serve/front/src/routes/(main)/apps/app-form.svelte index 80a1878f..56ff9f68 100644 --- a/cmd/serve/front/src/routes/(main)/apps/app-form.svelte +++ b/cmd/serve/front/src/routes/(main)/apps/app-form.svelte @@ -18,6 +18,7 @@ import type { Target } from '$lib/resources/targets'; import l from '$lib/localization'; import Dropdown, { type DropdownOption } from '$components/dropdown.svelte'; + import ServiceUrl from './service-url.svelte'; export let handler: (data: any) => Promise; export let targets: Target[]; @@ -40,8 +41,8 @@ let prodScheme = l.translate('app.how.placeholder.scheme'); let prodUrl = l.translate('app.how.placeholder.url'); - let stagingScheme = l.translate('app.how.placeholder.scheme'); - let stagingUrl = l.translate('app.how.placeholder.url'); + let stagingScheme = prodScheme; + let stagingUrl = prodUrl; const targetsMap = targets.reduce>((acc, value) => { acc[value.id] = value; @@ -51,19 +52,23 @@ $: appName = name || l.translate('app.how.placeholder.name'); $: { try { - const u = new URL(targetsMap[production.target]?.url); + const u = new URL(targetsMap[production.target]?.url!); prodScheme = u.protocol + '//'; prodUrl = u.hostname; - } catch {} + } catch { + prodScheme = prodUrl = ''; + } } $: { try { - const u = new URL(targetsMap[staging.target]?.url); + const u = new URL(targetsMap[staging.target]?.url!); stagingScheme = u.protocol + '//'; stagingUrl = u.hostname; - } catch {} + } catch { + stagingScheme = stagingUrl = ''; + } } const environmentText = l.translate('app.how.env'); @@ -73,7 +78,7 @@ const targetsOptions = targets.map((target) => ({ value: target.id, - label: `${target.url} - ${target.name}` + label: `${target.url ?? l.translate('target.manual_proxy')} - ${target.name}` })) satisfies DropdownOption[]; // 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/cmd/serve/healthcheck.go b/cmd/serve/healthcheck.go index 2ef90f9c..94440a17 100644 --- a/cmd/serve/healthcheck.go +++ b/cmd/serve/healthcheck.go @@ -11,7 +11,7 @@ type healthCheckResponse struct { } func (s *server) healthcheckHandler(ctx *gin.Context) { - http.Ok(ctx, healthCheckResponse{ + _ = http.Ok(ctx, healthCheckResponse{ Version: version.Current(), }) } diff --git a/cmd/serve/middlewares.go b/cmd/serve/middlewares.go index a2b59313..3e0b4a48 100644 --- a/cmd/serve/middlewares.go +++ b/cmd/serve/middlewares.go @@ -31,7 +31,7 @@ func (s *server) authenticate(withApiAccess bool) gin.HandlerFunc { // If it failed and api access is not allowed, return early if failed && !withApiAccess { - ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized) + _ = ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized) return } @@ -46,14 +46,14 @@ func (s *server) authenticate(withApiAccess bool) gin.HandlerFunc { authHeader := ctx.GetHeader(apiAuthHeader) if !strings.HasPrefix(authHeader, apiAuthPrefix) { - ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized) + _ = ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized) return } id, err := s.usersReader.GetIDFromAPIKey(ctx.Request.Context(), domain.APIKey(authHeader[apiAuthPrefixLength:])) if err != nil { - ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized) + _ = ctx.AbortWithError(http.StatusUnauthorized, errUnauthorized) return } @@ -66,8 +66,8 @@ func (s *server) authenticate(withApiAccess bool) gin.HandlerFunc { func (s *server) requestLogger(ctx *gin.Context) { defer func(start time.Time, c *gin.Context) { - path := ctx.Request.URL.Path - raw := ctx.Request.URL.RawQuery + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery if raw != "" { path = path + "?" + raw diff --git a/cmd/serve/server.go b/cmd/serve/server.go index 6368a91e..d1b18f63 100644 --- a/cmd/serve/server.go +++ b/cmd/serve/server.go @@ -34,6 +34,7 @@ type ( ServerOptions interface { Secret() []byte IsSecure() bool + IsDebug() bool ListenAddress() string } @@ -59,17 +60,21 @@ func newHttpServer(options ServerOptions, root startup.ServerRoot) *server { logger: root.Logger(), } - s.router.SetTrustedProxies(nil) + _ = s.router.SetTrustedProxies(nil) // Configure the session store - store := cookie.NewStore(s.options.Secret()) + store := cookie.NewStore(options.Secret()) store.Options(sessions.Options{ - Secure: s.options.IsSecure(), + Secure: options.IsSecure(), HttpOnly: true, SameSite: http.SameSiteStrictMode, }) - s.router.Use(s.requestLogger, s.recoverer, sessions.Sessions(sessionName, store)) + if options.IsDebug() { + s.router.Use(s.requestLogger) + } + + s.router.Use(s.recoverer, sessions.Sessions(sessionName, store)) // Let's register every routes now! v1 := s.router.Group("/api/v1") diff --git a/cmd/startup/server.go b/cmd/startup/server.go index bbf7fe29..723ce15b 100644 --- a/cmd/startup/server.go +++ b/cmd/startup/server.go @@ -47,7 +47,6 @@ type ( } serverRoot struct { - options ServerOptions bus bus.Bus logger log.Logger db *sqlite.Database @@ -61,13 +60,15 @@ type ( // needed by the server. func Server(options ServerOptions, logger log.Logger) (ServerRoot, error) { s := &serverRoot{ - options: options, - logger: logger, + logger: logger, } + // embedded.NewBus() + // embedded.NewScheduler() + s.bus = memory.NewBus() - db, err := sqlite.Open(s.options.ConnectionString(), s.logger, s.bus) + db, err := sqlite.Open(options.ConnectionString(), s.logger, s.bus) if err != nil { return nil, err @@ -81,13 +82,13 @@ func Server(options ServerOptions, logger log.Logger) (ServerRoot, error) { return nil, err } - s.scheduler = bus.NewScheduler(s.schedulerStore, s.logger, s.bus, s.options.RunnersPollInterval(), + s.scheduler = bus.NewScheduler(s.schedulerStore, s.logger, s.bus, options.RunnersPollInterval(), bus.WorkerGroup{ - Size: s.options.RunnersDeploymentCount(), + Size: options.RunnersDeploymentCount(), Messages: []string{deploy.Command{}.Name_()}, }, bus.WorkerGroup{ - Size: s.options.RunnersCleanupCount(), + Size: options.RunnersCleanupCount(), Messages: []string{ cleanup_app.Command{}.Name_(), delete_app.Command{}.Name_(), @@ -105,7 +106,7 @@ func Server(options ServerOptions, logger log.Logger) (ServerRoot, error) { // Setups deployment infrastructure if err = deploymentinfra.Setup( - s.options, + options, s.logger, s.db, s.bus, @@ -125,7 +126,7 @@ func Server(options ServerOptions, logger log.Logger) (ServerRoot, error) { } // Create the target needed to expose seelf itself and manage certificates if needed - if exposedUrl, isSet := s.options.AppExposedUrl().TryGet(); isSet { + if exposedUrl, isSet := options.AppExposedUrl().TryGet(); isSet { container := exposedUrl.User().Get("") s.logger.Infow("exposing seelf container using the local target, creating it if needed, the container may restart once done", diff --git a/cmd/version/version.go b/cmd/version/version.go index c30f1894..387778dc 100644 --- a/cmd/version/version.go +++ b/cmd/version/version.go @@ -4,7 +4,7 @@ import ( "runtime/debug" ) -var version = "2.3.2" +var version = "2.4.0-next.1" // Retrieve the currentVersion version with additional vcs info if any. func Current() string { diff --git a/docs/contributing/backend.md b/docs/contributing/backend.md index 3582a021..877ab1f8 100644 --- a/docs/contributing/backend.md +++ b/docs/contributing/backend.md @@ -7,7 +7,7 @@ The **seelf** backend is written in the [Golang](https://go.dev/) language for i ### Packages overview - `cmd/`: contains application commands such as the `serve` one -- `internal/`: contains internal package representing the **core features** of this application organized by bounded contexts and `app`, `domain` and `infra` folders (see [The Domain](#the-domain)) +- `internal/`: contains internal package representing the **core features** of this application organized by bounded contexts and `app`, `domain`, `infra` and `fixture` folders (see [The Domain](#the-domain)) - `pkg/`: contains reusable stuff not tied to seelf which can be reused if needed ### The Domain {#the-domain} @@ -19,6 +19,7 @@ The `internal/` follows a classic DDD structure with: - `app`: commands and queries to orchestrate the domain logic - `domain`: core stuff, entities and values objects, as pure as possible to be easily testable - `infra`: implementation of domain specific interfaces for the current context +- `fixture`: test helpers, mostly for generating correct and random aggregates satisfying needed state In Go, it's common to see entities as structs with every field exposed. In this project, I have decided to try something else to prevent unwanted mutations from happening and making things more explicit. diff --git a/docs/guide/migration.md b/docs/guide/migration.md index b416d8ff..b0976782 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -5,7 +5,7 @@ Most of the time, you don't have to act manually when upgrading **seelf**. Howev ## From v1 to v2 {#v2} ::: warning -This migration assumes you have **at least one application** on your seelf instance. If you do not have any application yet, you **should** remove everything and go back to [installing seelf](/guide/installation). +This migration assumes you have **at least one application you care about** on your seelf instance. If you do not have any application yet, you **should** remove everything and go back to [installing seelf](/guide/installation) since it will be easier. ::: The **seelf** `v2` introduces some breaking changes. When coming from the `v1.x.x`, you will need to take manual actions. diff --git a/docs/guide/updating.md b/docs/guide/updating.md index 280a96b5..d37b6960 100644 --- a/docs/guide/updating.md +++ b/docs/guide/updating.md @@ -6,6 +6,10 @@ This procedure depends on the method you choose when [installing seelf](/guide/i When switching from a major version to another one (ex. `v1.x.x` to `v2.x.x`), check the [Migration page](/guide/migration) for additional instructions. ::: +::: warning +You should always **make a backup** before updating **seelf** to make sure you don't lose anything if something goes wrong. +::: + ## With Compose Go where the initial `compose.yml` file has been created and run: @@ -28,4 +32,4 @@ docker pull yuukanoo/seelf && docker rm $(docker stop $(docker ps -a -q --filter ## From sources -Simply build the application again with the latest sources and you're good to go. +Simply build the application again with the latest sources, run it and you're good to go. 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/go.mod b/go.mod index c3c0ce8a..8bd03669 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,22 @@ module github.com/YuukanOO/seelf -go 1.21 +go 1.23 require ( - github.com/compose-spec/compose-go/v2 v2.0.3-0.20240407191136-f388192b8a39 - github.com/docker/cli v26.0.1+incompatible - github.com/docker/compose/v2 v2.26.1 - github.com/docker/docker v26.0.1+incompatible + github.com/compose-spec/compose-go/v2 v2.1.6 + github.com/docker/cli v27.1.2+incompatible + github.com/docker/compose/v2 v2.29.2 + github.com/docker/docker v27.1.2+incompatible github.com/docker/go-connections v0.5.0 - github.com/gin-gonic/gin v1.9.1 - github.com/go-git/go-git/v5 v5.11.0 - github.com/golang-migrate/migrate/v4 v4.16.2 + github.com/gin-gonic/gin v1.10.0 + github.com/go-git/go-git/v5 v5.12.0 + github.com/golang-migrate/migrate/v4 v4.17.1 github.com/joho/godotenv v1.5.1 github.com/kevinburke/ssh_config v1.2.0 - github.com/mattn/go-sqlite3 v1.14.19 + github.com/mattn/go-sqlite3 v1.14.22 github.com/segmentio/ksuid v1.0.4 - github.com/spf13/cobra v1.8.0 - go.uber.org/zap v1.26.0 + github.com/spf13/cobra v1.8.1 + go.uber.org/zap v1.27.0 ) require ( @@ -25,9 +25,8 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/Microsoft/hcsshim v0.11.4 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect @@ -44,24 +43,29 @@ require ( github.com/aws/smithy-go v1.19.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/goterm v1.0.4 // indirect - github.com/bytedance/sonic v1.10.2 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect - github.com/chenzhuoyu/iasm v0.9.1 // indirect - github.com/cloudflare/circl v1.3.6 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect github.com/containerd/console v1.0.4 // indirect - github.com/containerd/containerd v1.7.13 // indirect + github.com/containerd/containerd v1.7.20 // indirect + github.com/containerd/containerd/api v1.7.19 // indirect github.com/containerd/continuity v0.4.3 // indirect + github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/ttrpc v1.2.2 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/containerd/ttrpc v1.2.5 // indirect github.com/containerd/typeurl/v2 v2.1.1 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/distribution/reference v0.5.0 // indirect - github.com/docker/buildx v0.13.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/buildx v0.16.2 // indirect + github.com/docker/cli-docs-tool v0.8.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.0 // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -69,18 +73,19 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsevents v0.1.1 // indirect + github.com/fsnotify/fsevents v0.2.0 // indirect github.com/fvbommel/sortorder v1.0.2 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/gofrs/flock v0.8.1 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -94,18 +99,17 @@ require ( github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/in-toto/in-toto-golang v0.5.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/klauspost/compress v1.17.4 // indirect - github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -113,10 +117,9 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/miekg/pkcs11 v1.1.1 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/buildkit v0.13.1 // indirect + github.com/moby/buildkit v0.15.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect @@ -126,6 +129,7 @@ require ( github.com/moby/sys/signal v0.7.0 // indirect github.com/moby/sys/symlink v0.2.0 // indirect github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -133,30 +137,31 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.44.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect - github.com/sergi/go-diff v1.3.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.2.1 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.8.4 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect - github.com/tonistiigi/fsutil v0.0.0-20240301111122-7525a1af2bb5 // indirect + github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c // indirect + github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect - github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 // indirect + github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -167,12 +172,11 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.42.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/sdk v1.21.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect @@ -180,25 +184,21 @@ require ( go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/arch v0.6.0 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/oauth2 v0.11.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/term v0.16.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/term v0.20.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.17.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect - google.golang.org/grpc v1.59.0 // indirect + google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect + google.golang.org/grpc v1.60.1 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect k8s.io/api v0.29.2 // indirect k8s.io/apimachinery v0.29.2 // indirect - k8s.io/apiserver v0.29.2 // indirect k8s.io/client-go v0.29.2 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect @@ -206,7 +206,7 @@ require ( sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect - tags.cncf.io/container-device-interface v0.6.2 // indirect + tags.cncf.io/container-device-interface v0.7.2 // indirect ) require ( @@ -215,28 +215,28 @@ require ( github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.16.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.2.4 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.18.0 + golang.org/x/crypto v0.23.0 golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 - golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 ) replace ( - github.com/docker/compose/v2 => github.com/YuukanOO/compose/v2 v2.0.0-20240416104623-b2a6b2de620d // Skip network aliases if external network (which is the case for targets proxy networks). It will prevent dns conflicts when resolving + github.com/docker/compose/v2 => github.com/YuukanOO/compose/v2 v2.27.2-0.20240821071151-110203bb5f25 // Skip network aliases if external network (which is the case for targets proxy networks). It will prevent dns conflicts when resolving github.com/kevinburke/ssh_config => github.com/YuukanOO/ssh_config v0.0.0-20240416065040-22ccaddd8792 // Expose some additional stuff such as IsImplicit ) diff --git a/go.sum b/go.sum index 7ea9a49d..141106e5 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,7 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME= -cloud.google.com/go/compute v1.23.1 h1:V97tBoDaZHb6leicZ1G6DLK2BAaZLJ/7+9BB/En3hR0= -cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= @@ -19,21 +17,21 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= -github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= +github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb h1:w9IDEB7P1VzNcBpOG7kMpFkZp2DkyJIUt0gDx5MBhRU= github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= -github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= -github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/YuukanOO/compose/v2 v2.0.0-20240416104623-b2a6b2de620d h1:vtJ/MgHwydMofH9DqaRxvFtJHXFcFEfFNI3uU6bvLNk= -github.com/YuukanOO/compose/v2 v2.0.0-20240416104623-b2a6b2de620d/go.mod h1:yIsLkV74jorkKFiA9szQUe9qA90r6raAK9uu10va0Qk= +github.com/YuukanOO/compose/v2 v2.27.2-0.20240821071151-110203bb5f25 h1:sXyAIIJ42hbXuDgjo001SGp0ykcmwgY/1FaUtxZeLMc= +github.com/YuukanOO/compose/v2 v2.27.2-0.20240821071151-110203bb5f25/go.mod h1:umytvxgECSU214WiMKqVtf7LXsAeLb0I/UFGYW5SPDU= github.com/YuukanOO/ssh_config v0.0.0-20240416065040-22ccaddd8792 h1:INERq367kk1TRX9bsQkcKZdR801QVxS0vzDvTFLPuAE= github.com/YuukanOO/ssh_config v0.0.0-20240416065040-22ccaddd8792/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -93,80 +91,82 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= -github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= -github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= -github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= -github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= -github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/compose-spec/compose-go/v2 v2.0.3-0.20240407191136-f388192b8a39 h1:ZUpnv0xA75X9gy9Y7hjJm51nflGbr+2URaLXBtEic7A= -github.com/compose-spec/compose-go/v2 v2.0.3-0.20240407191136-f388192b8a39/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc= +github.com/compose-spec/compose-go/v2 v2.1.6 h1:d0Cs0DffmOwmSzs0YPHwKCskknGq2jfGg4uGowlEpps= +github.com/compose-spec/compose-go/v2 v2.1.6/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd v1.7.13 h1:wPYKIeGMN8vaggSKuV1X0wZulpMz4CrgEsZdaCyB6Is= -github.com/containerd/containerd v1.7.13/go.mod h1:zT3up6yTRfEUa6+GsITYIJNgSVL9NQ4x4h1RPzk0Wu4= +github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ= +github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0= +github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA= +github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nydus-snapshotter v0.13.7 h1:x7DHvGnzJOu1ZPwPYkeOPk5MjZZYbdddygEjaSDoFTk= github.com/containerd/nydus-snapshotter v0.13.7/go.mod h1:VPVKQ3jmHFIcUIV2yiQ1kImZuBFS3GXDohKs9mRABVE= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/containerd/stargz-snapshotter v0.15.1 h1:fpsP4kf/Z4n2EYnU0WT8ZCE3eiKDwikDhL6VwxIlgeA= github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= -github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtOs= -github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak= +github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= +github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/buildx v0.13.1 h1:uZjBcb477zh02tnHk0rqNV/DZOxbf/OiHw6Mc8OhDYU= -github.com/docker/buildx v0.13.1/go.mod h1:f2n6vggoX4sNNZ0XoRZ0Wtv6J1/rbDTabgdHtpW9NNM= -github.com/docker/cli v26.0.1+incompatible h1:eZDuplk2jYqgUkNLDYwTBxqmY9cM3yHnmN6OIUEjL3U= -github.com/docker/cli v26.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/buildx v0.16.2 h1:SPcyEiiCZEntJQ+V0lJI8ZudUrki2v1qUqmC/NqxDDs= +github.com/docker/buildx v0.16.2/go.mod h1:by+CuE4Q+2NvECkIhNcWe89jjbHADCrDlzS9MRgbv2k= +github.com/docker/cli v27.1.2+incompatible h1:nYviRv5Y+YAKx3dFrTvS1ErkyVVunKOhoweCTE1BsnI= +github.com/docker/cli v27.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli-docs-tool v0.8.0 h1:YcDWl7rQJC3lJ7WVZRwSs3bc9nka97QLWfyJQli8yJU= +github.com/docker/cli-docs-tool v0.8.0/go.mod h1:8TQQ3E7mOXoYUs811LiPdUnAhXrcVsBIrW21a5pUbdk= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v26.0.1+incompatible h1:t39Hm6lpXuXtgkF0dm1t9a5HkbUfdGy6XbWexmGr+hA= -github.com/docker/docker v26.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= -github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= +github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY= +github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= @@ -190,17 +190,13 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsevents v0.1.1 h1:/125uxJvvoSDDBPen6yUZbil8J9ydKZnnl3TWWmvnkw= -github.com/fsnotify/fsevents v0.1.1/go.mod h1:+d+hS27T6k5J8CRaPLKFgwKYcpS7GwW3Ule9+SC2ZRc= +github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c= +github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo= github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= @@ -211,10 +207,10 @@ github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NY github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= @@ -222,15 +218,15 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+ github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= -github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= @@ -249,49 +245,47 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= -github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= -github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= +github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= +github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93 h1:jc2UWq7CbdszqeH6qu1ougXMIUBfSy8Pbh/anURYbGI= github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -299,8 +293,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk= -github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -319,8 +313,6 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -332,8 +324,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -369,11 +361,11 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -387,11 +379,12 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.5.3 h1:C8fxWnhYyME3n0klPOhVM7PtYUB3eV1W3DeFmN3j53Y= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -409,8 +402,8 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= -github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -420,15 +413,13 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyex github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/buildkit v0.13.1 h1:L8afOFhPq2RPJJSr/VyzbufwID7jquZVB7oFHbPRcPE= -github.com/moby/buildkit v0.13.1/go.mod h1:aNmNQKLBFYAOFuzQjR3VA27/FijlvtBD1pjNwTSN37k= +github.com/moby/buildkit v0.15.1 h1:J6wrew7hphKqlq1wuu6yaUb/1Ra7gEzDAovylGztAKM= +github.com/moby/buildkit v0.15.1/go.mod h1:Yis8ZMUJTHX9XhH9zVyK2igqSHV3sxi3UN0uztZocZk= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -447,6 +438,8 @@ github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZ github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -479,16 +472,16 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= -github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= +github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -506,7 +499,6 @@ github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+L github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= @@ -518,9 +510,8 @@ github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc h1:zAsgcP8MhzAbhMnB1QQ2O7ZhWYVGYSR2iVcjzQuPV+o= github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8= @@ -533,8 +524,8 @@ github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/ github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= @@ -542,13 +533,11 @@ github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY= @@ -556,8 +545,8 @@ github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/u github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk= github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE= github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -570,6 +559,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -579,25 +569,27 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g= -github.com/tonistiigi/fsutil v0.0.0-20240301111122-7525a1af2bb5 h1:oZS8KCqAg62sxJkEq/Ppzqrb6EooqzWtL8Oaex7bc5c= -github.com/tonistiigi/fsutil v0.0.0-20240301111122-7525a1af2bb5/go.mod h1:vbbYqJlnswsbJqWUcJN8fKtBhnEgldDrcagTgnBVKKM= +github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c h1:+6wg/4ORAbnSoGDzg2Q1i3CeMcT/jjhye/ZfnBHy7/M= +github.com/tonistiigi/fsutil v0.0.0-20240424095704-91a3fc46842c/go.mod h1:vbbYqJlnswsbJqWUcJN8fKtBhnEgldDrcagTgnBVKKM= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0= github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= -github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 h1:Y/M5lygoNPKwVNLMPXgVfsRT40CSFKXCxuU8LoHySjs= -github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= +github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= +github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -624,8 +616,8 @@ go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 h1:ZtfnDL+tUrs1F0Pzfwbg2d59Gru9NCH3bgSHBM6LDwU= go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0/go.mod h1:hG4Fj/y8TR/tlEDREo8tWstl9fO9gcFkn4xrx0Io8xU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 h1:NmnYCiR0qNufkldjVvyQfZTHSdzeHoZ41zggMsdMcLM= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0/go.mod h1:UVAO61+umUsHLtYb8KXXRoHtxUkdOPkYidzW3gipRLQ= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 h1:wNMDy/LVGLj2h3p6zg4d0gypKfWKSWI14E1C4smOgl8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0/go.mod h1:YfbDdXAAkemWJK3H/DshvlrxqFB2rtW4rY6ky/3x/H0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= @@ -634,8 +626,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqhe go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= -go.opentelemetry.io/otel/exporters/prometheus v0.42.0 h1:jwV9iQdvp38fxXi8ZC+lNpxjK16MRcZlpDYvbuO1FiA= -go.opentelemetry.io/otel/exporters/prometheus v0.42.0/go.mod h1:f3bYiqNqhoPxkvI2LrXqQVC546K7BuRDL/kKuxkujhA= go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= @@ -646,22 +636,19 @@ go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8 go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= -golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -673,28 +660,19 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -706,30 +684,26 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= -golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -752,16 +726,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -770,15 +744,11 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -790,34 +760,19 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA= -google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= @@ -849,14 +804,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= -k8s.io/apiserver v0.29.2 h1:+Z9S0dSNr+CjnVXQePG8TcBWHr3Q7BmAr7NraHvsMiQ= -k8s.io/apiserver v0.29.2/go.mod h1:B0LieKVoyU7ykQvPFm7XSdIHaCHSzCzQWPFa5bqbeMQ= k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= @@ -873,5 +824,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+s sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= -tags.cncf.io/container-device-interface v0.6.2 h1:dThE6dtp/93ZDGhqaED2Pu374SOeUkBfuvkLuiTdwzg= -tags.cncf.io/container-device-interface v0.6.2/go.mod h1:Shusyhjs1A5Na/kqPVLL0KqnHQHuunol9LFeUNkuGVE= +tags.cncf.io/container-device-interface v0.7.2 h1:MLqGnWfOr1wB7m08ieI4YJ3IoLKKozEnnNYBtacDPQU= +tags.cncf.io/container-device-interface v0.7.2/go.mod h1:Xb1PvXv2BhfNb3tla4r9JL129ck1Lxv9KuU6eVOfKto= diff --git a/internal/auth/app/create_first_account/create_first_account_test.go b/internal/auth/app/create_first_account/create_first_account_test.go index 2cc8392a..a2d8fb25 100644 --- a/internal/auth/app/create_first_account/create_first_account_test.go +++ b/internal/auth/app/create_first_account/create_first_account_test.go @@ -6,71 +6,83 @@ import ( "github.com/YuukanOO/seelf/internal/auth/app/create_first_account" "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/auth/infra/crypto" - "github.com/YuukanOO/seelf/internal/auth/infra/memory" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" "github.com/YuukanOO/seelf/pkg/validate" ) func Test_CreateFirstAccount(t *testing.T) { - ctx := context.Background() - hasher := crypto.NewBCryptHasher() - keygen := crypto.NewKeyGenerator() - sut := func(existingUsers ...*domain.User) bus.RequestHandler[string, create_first_account.Command] { - store := memory.NewUsersStore(existingUsers...) - return create_first_account.Handler(store, store, hasher, keygen) + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[string, create_first_account.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return create_first_account.Handler(context.UsersStore, context.UsersStore, crypto.NewBCryptHasher(), crypto.NewKeyGenerator()), context.Dispatcher } t.Run("should returns the existing user id if a user already exists", func(t *testing.T) { - usr := must.Panic(domain.NewUser(domain.NewEmailRequirement("existing@example.com", true), "password", "apikey")) - uc := sut(&usr) + existingUser := fixture.User() + handler, dispatcher := arrange(t, fixture.WithUsers(&existingUser)) - uid, err := uc(ctx, create_first_account.Command{}) + uid, err := handler(context.Background(), create_first_account.Command{}) - testutil.IsNil(t, err) - testutil.Equals(t, string(usr.ID()), uid) + assert.Nil(t, err) + assert.Equal(t, string(existingUser.ID()), uid) + assert.HasLength(t, 0, dispatcher.Signals()) }) t.Run("should require both email and password or fail with ErrAdminAccountRequired", func(t *testing.T) { - uc := sut() - uid, err := uc(ctx, create_first_account.Command{}) + handler, _ := arrange(t) + uid, err := handler(context.Background(), create_first_account.Command{}) - testutil.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err) - testutil.Equals(t, "", uid) + assert.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err) + assert.Equal(t, "", uid) - uid, err = uc(ctx, create_first_account.Command{Email: "admin@example.com"}) - testutil.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err) - testutil.Equals(t, "", uid) - - uid, err = uc(ctx, create_first_account.Command{Password: "admin"}) - testutil.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err) - testutil.Equals(t, "", uid) + uid, err = handler(context.Background(), create_first_account.Command{Email: "admin@example.com"}) + assert.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err) + assert.Equal(t, "", uid) + uid, err = handler(context.Background(), create_first_account.Command{Password: "admin"}) + assert.ErrorIs(t, create_first_account.ErrAdminAccountRequired, err) + assert.Equal(t, "", uid) }) t.Run("should require valid inputs", func(t *testing.T) { - uc := sut() - uid, err := uc(ctx, create_first_account.Command{ - Email: "notanemail", + handler, _ := arrange(t) + uid, err := handler(context.Background(), create_first_account.Command{ + Email: "not_an_email", Password: "admin", }) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - testutil.Equals(t, "", uid) - + assert.Equal(t, "", uid) + assert.ValidationError(t, validate.FieldErrors{ + "email": domain.ErrInvalidEmail, + }, err) }) t.Run("should creates the first user account if everything is good", func(t *testing.T) { - uc := sut() - uid, err := uc(ctx, create_first_account.Command{ + handler, dispatcher := arrange(t) + uid, err := handler(context.Background(), create_first_account.Command{ Email: "admin@example.com", Password: "admin", }) - testutil.IsNil(t, err) - testutil.NotEquals(t, "", uid) + assert.Nil(t, err) + assert.NotEqual(t, "", uid) + + assert.HasLength(t, 1, dispatcher.Signals()) + registered := assert.Is[domain.UserRegistered](t, dispatcher.Signals()[0]) + + assert.Equal(t, domain.UserRegistered{ + ID: domain.UserID(uid), + Email: "admin@example.com", + Password: assert.NotZero(t, registered.Password), + RegisteredAt: assert.NotZero(t, registered.RegisteredAt), + Key: assert.NotZero(t, registered.Key), + }, registered) }) } diff --git a/internal/auth/app/login/login_test.go b/internal/auth/app/login/login_test.go index a304d4c5..70a011de 100644 --- a/internal/auth/app/login/login_test.go +++ b/internal/auth/app/login/login_test.go @@ -6,66 +6,81 @@ import ( "github.com/YuukanOO/seelf/internal/auth/app/login" "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/auth/infra/crypto" - "github.com/YuukanOO/seelf/internal/auth/infra/memory" - "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" "github.com/YuukanOO/seelf/pkg/validate" + "github.com/YuukanOO/seelf/pkg/validate/strings" ) func Test_Login(t *testing.T) { hasher := crypto.NewBCryptHasher() - password := must.Panic(hasher.Hash("password")) // Sample password hash for the string "password" for tests - existingUser := must.Panic(domain.NewUser(domain.NewEmailRequirement("existing@example.com", true), password, "apikey")) - sut := func(existingUsers ...*domain.User) bus.RequestHandler[string, login.Command] { - store := memory.NewUsersStore(existingUsers...) - return login.Handler(store, hasher) + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[string, login.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return login.Handler(context.UsersStore, hasher), context.Dispatcher } t.Run("should require valid inputs", func(t *testing.T) { - uc := sut() - _, err := uc(context.Background(), login.Command{}) + handler, _ := arrange(t) + _, err := handler(context.Background(), login.Command{}) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) + assert.ValidationError(t, validate.FieldErrors{ + "email": domain.ErrInvalidEmail, + "password": strings.ErrRequired, + }, err) }) t.Run("should complains if email does not exists", func(t *testing.T) { - uc := sut() - _, err := uc(context.Background(), login.Command{ + handler, _ := arrange(t) + _, err := handler(context.Background(), login.Command{ Email: "notexisting@example.com", - Password: "nobodycares", + Password: "no_body_cares", }) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrInvalidEmailOrPassword, validationErr["email"]) - testutil.ErrorIs(t, domain.ErrInvalidEmailOrPassword, validationErr["password"]) + assert.ValidationError(t, validate.FieldErrors{ + "email": domain.ErrInvalidEmailOrPassword, + "password": domain.ErrInvalidEmailOrPassword, + }, err) }) t.Run("should complains if password does not match", func(t *testing.T) { - uc := sut(&existingUser) - _, err := uc(context.Background(), login.Command{ + existingUser := fixture.User( + fixture.WithEmail("existing@example.com"), + fixture.WithPassword("raw_password_hash", hasher), + ) + handler, _ := arrange(t, fixture.WithUsers(&existingUser)) + + _, err := handler(context.Background(), login.Command{ Email: "existing@example.com", - Password: "nobodycares", + Password: "no_body_cares", }) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrInvalidEmailOrPassword, validationErr["email"]) - testutil.ErrorIs(t, domain.ErrInvalidEmailOrPassword, validationErr["password"]) + assert.ValidationError(t, validate.FieldErrors{ + "email": domain.ErrInvalidEmailOrPassword, + "password": domain.ErrInvalidEmailOrPassword, + }, err) }) t.Run("should returns a valid user id if it succeeds", func(t *testing.T) { - uc := sut(&existingUser) - uid, err := uc(context.Background(), login.Command{ + existingUser := fixture.User( + fixture.WithEmail("existing@example.com"), + fixture.WithPassword("password", hasher), + ) + handler, dispatcher := arrange(t, fixture.WithUsers(&existingUser)) + + uid, err := handler(context.Background(), login.Command{ Email: "existing@example.com", Password: "password", }) - testutil.IsNil(t, err) - testutil.Equals(t, string(existingUser.ID()), uid) + assert.Nil(t, err) + assert.Equal(t, string(existingUser.ID()), uid) + assert.HasLength(t, 0, dispatcher.Signals()) }) } diff --git a/internal/auth/app/refresh_api_key/refresh_api_key_test.go b/internal/auth/app/refresh_api_key/refresh_api_key_test.go index bfe84bdc..1f6d8c63 100644 --- a/internal/auth/app/refresh_api_key/refresh_api_key_test.go +++ b/internal/auth/app/refresh_api_key/refresh_api_key_test.go @@ -6,43 +6,49 @@ import ( "github.com/YuukanOO/seelf/internal/auth/app/refresh_api_key" "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/auth/infra/crypto" - "github.com/YuukanOO/seelf/internal/auth/infra/memory" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" ) func Test_RefreshApiKey(t *testing.T) { - sut := func(existingUsers ...*domain.User) bus.RequestHandler[string, refresh_api_key.Command] { - store := memory.NewUsersStore(existingUsers...) - return refresh_api_key.Handler(store, store, crypto.NewKeyGenerator()) + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[string, refresh_api_key.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return refresh_api_key.Handler(context.UsersStore, context.UsersStore, crypto.NewKeyGenerator()), context.Dispatcher } t.Run("should fail if the user does not exists", func(t *testing.T) { - uc := sut() + handler, _ := arrange(t) - _, err := uc(context.Background(), refresh_api_key.Command{}) + _, err := handler(context.Background(), refresh_api_key.Command{}) - testutil.ErrorIs(t, apperr.ErrNotFound, err) + assert.ErrorIs(t, apperr.ErrNotFound, err) }) t.Run("should refresh the user's API key if everything is good", func(t *testing.T) { - user := must.Panic(domain.NewUser(domain.NewEmailRequirement("some@email.com", true), "someHashedPassword", "apikey")) - uc := sut(&user) + existingUser := fixture.User() + handler, dispatcher := arrange(t, fixture.WithUsers(&existingUser)) - key, err := uc(context.Background(), refresh_api_key.Command{ - ID: string(user.ID())}, + key, err := handler(context.Background(), refresh_api_key.Command{ + ID: string(existingUser.ID())}, ) - testutil.IsNil(t, err) - testutil.NotEquals(t, "", key) + assert.Nil(t, err) + assert.NotEqual(t, "", key) - evt := testutil.EventIs[domain.UserAPIKeyChanged](t, &user, 1) + assert.HasLength(t, 1, dispatcher.Signals()) + keyChanged := assert.Is[domain.UserAPIKeyChanged](t, dispatcher.Signals()[0]) - testutil.Equals(t, user.ID(), evt.ID) - testutil.Equals(t, key, string(evt.Key)) + assert.Equal(t, domain.UserAPIKeyChanged{ + ID: existingUser.ID(), + Key: domain.APIKey(key), + }, keyChanged) }) } diff --git a/internal/auth/app/update_user/update_user_test.go b/internal/auth/app/update_user/update_user_test.go index 386455cf..4124bb6b 100644 --- a/internal/auth/app/update_user/update_user_test.go +++ b/internal/auth/app/update_user/update_user_test.go @@ -6,79 +6,110 @@ import ( "github.com/YuukanOO/seelf/internal/auth/app/update_user" "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/auth/infra/crypto" - "github.com/YuukanOO/seelf/internal/auth/infra/memory" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" + "github.com/YuukanOO/seelf/pkg/bus/spy" "github.com/YuukanOO/seelf/pkg/monad" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" "github.com/YuukanOO/seelf/pkg/validate" ) func Test_UpdateUser(t *testing.T) { - hasher := crypto.NewBCryptHasher() - passwordHash := must.Panic(hasher.Hash("apassword")) - sut := func(existingUsers ...*domain.User) bus.RequestHandler[string, update_user.Command] { - store := memory.NewUsersStore(existingUsers...) - return update_user.Handler(store, store, hasher) + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[string, update_user.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return update_user.Handler(context.UsersStore, context.UsersStore, crypto.NewBCryptHasher()), context.Dispatcher } + t.Run("should require an existing user", func(t *testing.T) { + handler, _ := arrange(t) + _, err := handler(context.Background(), update_user.Command{}) + + assert.ErrorIs(t, apperr.ErrNotFound, err) + }) + t.Run("should require valid inputs", func(t *testing.T) { - uc := sut() - _, err := uc(context.Background(), update_user.Command{}) + handler, _ := arrange(t) - testutil.ErrorIs(t, apperr.ErrNotFound, err) + _, err := handler(context.Background(), update_user.Command{ + Email: monad.Value("notanemail"), + }) + + assert.ValidationError(t, validate.FieldErrors{ + "email": domain.ErrInvalidEmail, + }, err) }) t.Run("should fail if the email is taken by another user", func(t *testing.T) { - john := must.Panic(domain.NewUser(domain.NewEmailRequirement("john@doe.com", true), passwordHash, "anapikey")) - jane := must.Panic(domain.NewUser(domain.NewEmailRequirement("jane@doe.com", true), passwordHash, "anapikey")) + john := fixture.User(fixture.WithEmail("john@doe.com")) + jane := fixture.User(fixture.WithEmail("jane@doe.com")) - uc := sut(&john, &jane) + handler, _ := arrange(t, fixture.WithUsers(&john, &jane)) - _, err := uc(context.Background(), update_user.Command{ + _, err := handler(context.Background(), update_user.Command{ ID: string(john.ID()), Email: monad.Value("jane@doe.com"), }) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrEmailAlreadyTaken, validationErr["email"]) + assert.ValidationError(t, validate.FieldErrors{ + "email": domain.ErrEmailAlreadyTaken, + }, err) }) t.Run("should succeed if values are the same", func(t *testing.T) { - john := must.Panic(domain.NewUser(domain.NewEmailRequirement("john@doe.com", true), passwordHash, "anapikey")) - uc := sut(&john) + existingUser := fixture.User(fixture.WithEmail("john@doe.com")) + handler, dispatcher := arrange(t, fixture.WithUsers(&existingUser)) - id, err := uc(context.Background(), update_user.Command{ - ID: string(john.ID()), + id, err := handler(context.Background(), update_user.Command{ + ID: string(existingUser.ID()), Email: monad.Value("john@doe.com"), Password: monad.Value("apassword"), }) - testutil.IsNil(t, err) - testutil.Equals(t, string(john.ID()), id) - testutil.HasNEvents(t, &john, 2) // 2 since bcrypt will produce different hashes - testutil.EventIs[domain.UserPasswordChanged](t, &john, 1) + assert.Nil(t, err) + assert.Equal(t, string(existingUser.ID()), id) + + assert.HasLength(t, 1, dispatcher.Signals()) + changed := assert.Is[domain.UserPasswordChanged](t, dispatcher.Signals()[0]) + + assert.Equal(t, domain.UserPasswordChanged{ + ID: existingUser.ID(), + Password: changed.Password, + }, changed) }) t.Run("should update user if everything is good", func(t *testing.T) { - john := must.Panic(domain.NewUser(domain.NewEmailRequirement("john@doe.com", true), passwordHash, "anapikey")) - uc := sut(&john) + existingUser := fixture.User() + handler, dispatcher := arrange(t, fixture.WithUsers(&existingUser)) - id, err := uc(context.Background(), update_user.Command{ - ID: string(john.ID()), + id, err := handler(context.Background(), update_user.Command{ + ID: string(existingUser.ID()), Email: monad.Value("another@email.com"), Password: monad.Value("anotherpassword"), }) - testutil.IsNil(t, err) - testutil.Equals(t, string(john.ID()), id) - testutil.HasNEvents(t, &john, 3) - evt := testutil.EventIs[domain.UserEmailChanged](t, &john, 1) - testutil.Equals(t, "another@email.com", string(evt.Email)) - testutil.EventIs[domain.UserPasswordChanged](t, &john, 2) + assert.Nil(t, err) + assert.Equal(t, string(existingUser.ID()), id) + + assert.HasLength(t, 2, dispatcher.Signals()) + + passwordChanged := assert.Is[domain.UserPasswordChanged](t, dispatcher.Signals()[1]) + + assert.Equal(t, domain.UserPasswordChanged{ + ID: existingUser.ID(), + Password: passwordChanged.Password, + }, passwordChanged) + + emailChanged := assert.Is[domain.UserEmailChanged](t, dispatcher.Signals()[0]) + + assert.Equal(t, domain.UserEmailChanged{ + ID: existingUser.ID(), + Email: "another@email.com", + }, emailChanged) }) } diff --git a/internal/auth/domain/context_test.go b/internal/auth/domain/context_test.go index 0cecb5ad..b45566f7 100644 --- a/internal/auth/domain/context_test.go +++ b/internal/auth/domain/context_test.go @@ -5,22 +5,23 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/auth/domain" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" + "github.com/YuukanOO/seelf/pkg/monad" ) -func Test_Auth_Context(t *testing.T) { +func Test_AuthContext(t *testing.T) { t.Run("should embed a user id into the context", func(t *testing.T) { ctx := context.Background() - uid := domain.UserID("auserid") + uid := domain.UserID("a_user_id") newCtx := domain.WithUserID(ctx, uid) - testutil.Equals(t, uid, domain.CurrentUser(newCtx).MustGet()) + assert.Equal(t, uid, domain.CurrentUser(newCtx).MustGet()) }) t.Run("should returns an empty monad.Maybe if no user id has been attached to the context", func(t *testing.T) { uid := domain.CurrentUser(context.Background()) - testutil.IsFalse(t, uid.HasValue()) + assert.Equal(t, monad.None[domain.UserID](), uid) }) } diff --git a/internal/auth/domain/email_test.go b/internal/auth/domain/email_test.go index 7b8ac0d9..1026a04a 100644 --- a/internal/auth/domain/email_test.go +++ b/internal/auth/domain/email_test.go @@ -4,15 +4,17 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/auth/domain" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" ) func Test_Email_ValidatesAnEmail(t *testing.T) { r, err := domain.EmailFrom("") - testutil.Equals(t, "", r) - testutil.ErrorIs(t, domain.ErrInvalidEmail, err) + + assert.Equal(t, "", r) + assert.ErrorIs(t, domain.ErrInvalidEmail, err) r, err = domain.EmailFrom("agood@email.com") - testutil.Equals(t, "agood@email.com", r) - testutil.IsNil(t, err) + + assert.Equal(t, "agood@email.com", r) + assert.Nil(t, err) } diff --git a/internal/auth/domain/user_test.go b/internal/auth/domain/user_test.go index 76441676..df56cf94 100644 --- a/internal/auth/domain/user_test.go +++ b/internal/auth/domain/user_test.go @@ -4,14 +4,14 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/auth/domain" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/internal/auth/fixture" + "github.com/YuukanOO/seelf/pkg/assert" ) func Test_User(t *testing.T) { t.Run("should fail if the email is not available", func(t *testing.T) { _, err := domain.NewUser(domain.NewEmailRequirement("an@email.com", false), "password", "apikey") - testutil.Equals(t, domain.ErrEmailAlreadyTaken, err) + assert.ErrorIs(t, domain.ErrEmailAlreadyTaken, err) }) t.Run("could be created", func(t *testing.T) { @@ -23,58 +23,70 @@ func Test_User(t *testing.T) { u, err := domain.NewUser(domain.NewEmailRequirement(email, true), password, key) - testutil.IsNil(t, err) - testutil.Equals(t, password, u.Password()) - testutil.NotEquals(t, "", u.ID()) + assert.Nil(t, err) + assert.Equal(t, password, u.Password()) + assert.NotZero(t, u.ID()) - registeredEvent := testutil.EventIs[domain.UserRegistered](t, &u, 0) + registeredEvent := assert.EventIs[domain.UserRegistered](t, &u, 0) - testutil.Equals(t, u.ID(), registeredEvent.ID) - testutil.Equals(t, email, registeredEvent.Email) - testutil.Equals(t, u.Password(), registeredEvent.Password) - testutil.Equals(t, key, registeredEvent.Key) + assert.Equal(t, domain.UserRegistered{ + ID: u.ID(), + Email: email, + Password: password, + Key: key, + RegisteredAt: assert.NotZero(t, registeredEvent.RegisteredAt), + }, registeredEvent) }) t.Run("should fail if trying to change for a non available email", func(t *testing.T) { - u := must.Panic(domain.NewUser(domain.NewEmailRequirement("some@email.com", true), "someHashedPassword", "apikey")) + existingUser := fixture.User() - err := u.HasEmail(domain.NewEmailRequirement("one@email.com", false)) - testutil.Equals(t, domain.ErrEmailAlreadyTaken, err) + err := existingUser.HasEmail(domain.NewEmailRequirement("one@email.com", false)) + assert.ErrorIs(t, domain.ErrEmailAlreadyTaken, err) }) t.Run("should be able to change email", func(t *testing.T) { - u := must.Panic(domain.NewUser(domain.NewEmailRequirement("some@email.com", true), "someHashedPassword", "apikey")) + existingUser := fixture.User(fixture.WithEmail("some@email.com")) - u.HasEmail(domain.NewEmailRequirement("some@email.com", true)) // no change, should not trigger events - u.HasEmail(domain.NewEmailRequirement("newone@email.com", true)) + assert.Nil(t, existingUser.HasEmail(domain.NewEmailRequirement("some@email.com", true))) + assert.Nil(t, existingUser.HasEmail(domain.NewEmailRequirement("newone@email.com", true))) - testutil.HasNEvents(t, &u, 2) - evt := testutil.EventIs[domain.UserEmailChanged](t, &u, 1) - testutil.Equals(t, u.ID(), evt.ID) - testutil.Equals(t, "newone@email.com", evt.Email) + assert.HasNEvents(t, 2, &existingUser, "should raise the event once per different email") + evt := assert.EventIs[domain.UserEmailChanged](t, &existingUser, 1) + + assert.Equal(t, domain.UserEmailChanged{ + ID: existingUser.ID(), + Email: "newone@email.com", + }, evt) }) t.Run("should be able to change password", func(t *testing.T) { - u := must.Panic(domain.NewUser(domain.NewEmailRequirement("some@email.com", true), "someHashedPassword", "apikey")) + existingUser := fixture.User(fixture.WithPasswordHash("someHashedPassword")) + + existingUser.HasPassword("someHashedPassword") + existingUser.HasPassword("anotherPassword") - u.HasPassword("someHashedPassword") // no change, should not trigger events - u.HasPassword("anotherPassword") + assert.HasNEvents(t, 2, &existingUser, "should raise the event once per different password") + evt := assert.EventIs[domain.UserPasswordChanged](t, &existingUser, 1) - testutil.HasNEvents(t, &u, 2) - evt := testutil.EventIs[domain.UserPasswordChanged](t, &u, 1) - testutil.Equals(t, u.ID(), evt.ID) - testutil.Equals(t, "anotherPassword", evt.Password) + assert.Equal(t, domain.UserPasswordChanged{ + ID: existingUser.ID(), + Password: "anotherPassword", + }, evt) }) t.Run("should be able to change API key", func(t *testing.T) { - u := must.Panic(domain.NewUser(domain.NewEmailRequirement("some@email.com", true), "someHashedPassword", "apikey")) + existingUser := fixture.User(fixture.WithAPIKey("apikey")) + + existingUser.HasAPIKey("apikey") + existingUser.HasAPIKey("anotherKey") - u.HasAPIKey("apikey") // no change, should not trigger events - u.HasAPIKey("anotherKey") + assert.HasNEvents(t, 2, &existingUser, "should raise the event once per different API key") + evt := assert.EventIs[domain.UserAPIKeyChanged](t, &existingUser, 1) - testutil.HasNEvents(t, &u, 2) - evt := testutil.EventIs[domain.UserAPIKeyChanged](t, &u, 1) - testutil.Equals(t, u.ID(), evt.ID) - testutil.Equals(t, "anotherKey", evt.Key) + assert.Equal(t, domain.UserAPIKeyChanged{ + ID: existingUser.ID(), + Key: "anotherKey", + }, evt) }) } diff --git a/internal/auth/fixture/database.go b/internal/auth/fixture/database.go new file mode 100644 index 00000000..5b1cee60 --- /dev/null +++ b/internal/auth/fixture/database.go @@ -0,0 +1,88 @@ +//go:build !release + +package fixture + +import ( + "context" + "os" + "testing" + + "github.com/YuukanOO/seelf/cmd/config" + "github.com/YuukanOO/seelf/internal/auth/domain" + auth "github.com/YuukanOO/seelf/internal/auth/infra/sqlite" + "github.com/YuukanOO/seelf/pkg/bus/spy" + "github.com/YuukanOO/seelf/pkg/log" + "github.com/YuukanOO/seelf/pkg/must" + "github.com/YuukanOO/seelf/pkg/ostools" + "github.com/YuukanOO/seelf/pkg/storage/sqlite" +) + +type ( + seed struct { + users []*domain.User + } + + Context struct { + Context context.Context // If users has been seeded, will be authenticated as the first one + Dispatcher spy.Dispatcher + UsersStore auth.UsersStore + } + + SeedBuilder func(*seed) +) + +func PrepareDatabase(t testing.TB, options ...SeedBuilder) *Context { + cfg := config.Default(config.WithTestDefaults()) + + if err := ostools.MkdirAll(cfg.DataDir()); err != nil { + t.Fatal(err) + } + + result := Context{ + Context: context.Background(), + Dispatcher: spy.NewDispatcher(), + } + + db, err := sqlite.Open(cfg.ConnectionString(), must.Panic(log.NewLogger()), result.Dispatcher) + + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + db.Close() + os.RemoveAll(cfg.DataDir()) + }) + + if err = db.Migrate(auth.Migrations); err != nil { + t.Fatal(err) + } + + result.UsersStore = auth.NewUsersStore(db) + + // Seed the database + var s seed + + for _, o := range options { + o(&s) + } + + if len(s.users) > 0 { + if err := result.UsersStore.Write(result.Context, s.users...); err != nil { + t.Fatal(err) + } + + result.Context = domain.WithUserID(result.Context, s.users[0].ID()) // The first created user will be used as the authenticated one + } + + // Reset the dispatcher after seeding + result.Dispatcher.Reset() + + return &result +} + +func WithUsers(users ...*domain.User) SeedBuilder { + return func(s *seed) { + s.users = users + } +} diff --git a/internal/auth/fixture/database_test.go b/internal/auth/fixture/database_test.go new file mode 100644 index 00000000..05f9472c --- /dev/null +++ b/internal/auth/fixture/database_test.go @@ -0,0 +1,30 @@ +package fixture_test + +import ( + "testing" + + "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/internal/auth/fixture" + "github.com/YuukanOO/seelf/pkg/assert" +) + +func Test_PrepareDatabase(t *testing.T) { + t.Run("should be able to prepare a database without seeding it", func(t *testing.T) { + ctx := fixture.PrepareDatabase(t) + + assert.NotNil(t, ctx) + assert.NotNil(t, ctx.Dispatcher) + assert.NotNil(t, ctx.UsersStore) + assert.HasLength(t, 0, ctx.Dispatcher.Signals()) + assert.HasLength(t, 0, ctx.Dispatcher.Requests()) + }) + + t.Run("should seed users and attach the first user id to the created context", func(t *testing.T) { + user1 := fixture.User() + user2 := fixture.User() + + ctx := fixture.PrepareDatabase(t, fixture.WithUsers(&user1, &user2)) + + assert.Equal(t, user1.ID(), domain.CurrentUser(ctx.Context).Get("")) + }) +} diff --git a/internal/auth/fixture/user.go b/internal/auth/fixture/user.go new file mode 100644 index 00000000..bd19d4f7 --- /dev/null +++ b/internal/auth/fixture/user.go @@ -0,0 +1,61 @@ +//go:build !release + +package fixture + +import ( + "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/pkg/id" + "github.com/YuukanOO/seelf/pkg/must" +) + +type ( + userOption struct { + email domain.Email + passwordHash domain.PasswordHash + apiKey domain.APIKey + } + + UserOptionBuilder func(*userOption) +) + +func User(options ...UserOptionBuilder) domain.User { + opts := userOption{ + email: "john" + id.New[domain.Email]() + "@doe.com", + passwordHash: id.New[domain.PasswordHash](), + apiKey: id.New[domain.APIKey](), + } + + for _, o := range options { + o(&opts) + } + + return must.Panic(domain.NewUser( + domain.NewEmailRequirement(opts.email, true), + opts.passwordHash, + opts.apiKey, + )) +} + +func WithEmail(email domain.Email) UserOptionBuilder { + return func(o *userOption) { + o.email = email + } +} + +func WithPasswordHash(passwordHash domain.PasswordHash) UserOptionBuilder { + return func(o *userOption) { + o.passwordHash = passwordHash + } +} + +func WithPassword(password string, hasher domain.PasswordHasher) UserOptionBuilder { + return func(o *userOption) { + o.passwordHash = must.Panic(hasher.Hash(password)) + } +} + +func WithAPIKey(apiKey domain.APIKey) UserOptionBuilder { + return func(o *userOption) { + o.apiKey = apiKey + } +} diff --git a/internal/auth/fixture/user_test.go b/internal/auth/fixture/user_test.go new file mode 100644 index 00000000..ceac6e72 --- /dev/null +++ b/internal/auth/fixture/user_test.go @@ -0,0 +1,47 @@ +package fixture_test + +import ( + "testing" + + "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/internal/auth/fixture" + "github.com/YuukanOO/seelf/internal/auth/infra/crypto" + "github.com/YuukanOO/seelf/pkg/assert" +) + +func Test_User(t *testing.T) { + t.Run("should be able to create a random user", func(t *testing.T) { + user := fixture.User() + + assert.NotZero(t, user.ID()) + }) + + t.Run("should be able to create a user with a given email", func(t *testing.T) { + user := fixture.User(fixture.WithEmail("an@email.com")) + + registered := assert.EventIs[domain.UserRegistered](t, &user, 0) + assert.Equal(t, "an@email.com", registered.Email) + }) + + t.Run("should be able to create a user with a given password hash", func(t *testing.T) { + user := fixture.User(fixture.WithPasswordHash("somePassword")) + + registered := assert.EventIs[domain.UserRegistered](t, &user, 0) + assert.Equal(t, "somePassword", registered.Password) + }) + + t.Run("should be able to create a user with a given password", func(t *testing.T) { + hasher := crypto.NewBCryptHasher() + user := fixture.User(fixture.WithPassword("somePassword", hasher)) + + registered := assert.EventIs[domain.UserRegistered](t, &user, 0) + assert.Nil(t, hasher.Compare("somePassword", registered.Password)) + }) + + t.Run("should be able to create a user with a given api key", func(t *testing.T) { + user := fixture.User(fixture.WithAPIKey("someapikey")) + + registered := assert.EventIs[domain.UserRegistered](t, &user, 0) + assert.Equal(t, "someapikey", registered.Key) + }) +} diff --git a/internal/auth/infra/crypto/api_key_generator_test.go b/internal/auth/infra/crypto/api_key_generator_test.go index 0aa181f0..e6188538 100644 --- a/internal/auth/infra/crypto/api_key_generator_test.go +++ b/internal/auth/infra/crypto/api_key_generator_test.go @@ -4,14 +4,15 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/auth/infra/crypto" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" ) func Test_KeyGenerator(t *testing.T) { t.Run("should generate an API key", func(t *testing.T) { generator := crypto.NewKeyGenerator() key, err := generator.Generate() - testutil.IsNil(t, err) - testutil.HasNChars(t, 64, key) + + assert.Nil(t, err) + assert.HasNRunes(t, 64, key) }) } diff --git a/internal/auth/infra/crypto/bcrypt_hasher_test.go b/internal/auth/infra/crypto/bcrypt_hasher_test.go index 8cf4882c..e7cccead 100644 --- a/internal/auth/infra/crypto/bcrypt_hasher_test.go +++ b/internal/auth/infra/crypto/bcrypt_hasher_test.go @@ -4,25 +4,25 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/auth/infra/crypto" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" "golang.org/x/crypto/bcrypt" ) -var hasher = crypto.NewBCryptHasher() +func Test_BCryptHasher(t *testing.T) { + hasher := crypto.NewBCryptHasher() -func Test_BCryptHasher_ShouldHashPassword(t *testing.T) { t.Run("should hash password", func(t *testing.T) { hash, err := hasher.Hash("mysecretpassword") - testutil.IsNil(t, err) - testutil.HasNChars(t, 60, hash) + assert.Nil(t, err) + assert.HasNRunes(t, 60, hash) }) t.Run("should compare password", func(t *testing.T) { hash, _ := hasher.Hash("mysecretpassword") err := hasher.Compare("mysecretpassword", hash) - testutil.IsNil(t, err) + assert.Nil(t, err) err = hasher.Compare("anothersecretpassword", hash) - testutil.IsTrue(t, err == bcrypt.ErrMismatchedHashAndPassword) + assert.ErrorIs(t, bcrypt.ErrMismatchedHashAndPassword, err) }) } diff --git a/internal/auth/infra/memory/users.go b/internal/auth/infra/memory/users.go deleted file mode 100644 index 760061f2..00000000 --- a/internal/auth/infra/memory/users.go +++ /dev/null @@ -1,126 +0,0 @@ -package memory - -import ( - "context" - "errors" - "slices" - - "github.com/YuukanOO/seelf/internal/auth/domain" - "github.com/YuukanOO/seelf/pkg/apperr" - "github.com/YuukanOO/seelf/pkg/event" -) - -type ( - UsersStore interface { - domain.UsersReader - domain.UsersWriter - } - - usersStore struct { - users []*userData - } - - userData struct { - id domain.UserID - key domain.APIKey - email domain.Email - value *domain.User - } -) - -func NewUsersStore(existingUsers ...*domain.User) UsersStore { - s := &usersStore{} - - s.Write(context.Background(), existingUsers...) - - return s -} - -func (s *usersStore) GetAdminUser(ctx context.Context) (domain.User, error) { - if len(s.users) == 0 { - return domain.User{}, apperr.ErrNotFound - } - - return *s.users[0].value, nil -} - -func (s *usersStore) CheckEmailAvailability(ctx context.Context, email domain.Email, excluded ...domain.UserID) (domain.EmailRequirement, error) { - u, err := s.GetByEmail(ctx, email) - - return domain.NewEmailRequirement(email, errors.Is(err, apperr.ErrNotFound) || slices.Contains(excluded, u.ID())), nil -} - -func (s *usersStore) GetByID(ctx context.Context, id domain.UserID) (domain.User, error) { - for _, u := range s.users { - if u.id == id { - return *u.value, nil - } - } - - return domain.User{}, apperr.ErrNotFound -} - -func (s *usersStore) GetByEmail(ctx context.Context, email domain.Email) (domain.User, error) { - for _, u := range s.users { - if u.email == email { - return *u.value, nil - } - } - - return domain.User{}, apperr.ErrNotFound -} - -func (s *usersStore) GetIDFromAPIKey(ctx context.Context, key domain.APIKey) (domain.UserID, error) { - for _, u := range s.users { - if u.key == key { - return u.id, nil - } - } - - return "", apperr.ErrNotFound -} - -func (s *usersStore) Write(ctx context.Context, users ...*domain.User) error { - for _, user := range users { - for _, e := range event.Unwrap(user) { - switch evt := e.(type) { - case domain.UserRegistered: - var exist bool - for _, a := range s.users { - if a.id == evt.ID { - exist = true - break - } - } - - if exist { - continue - } - - s.users = append(s.users, &userData{ - id: evt.ID, - email: evt.Email, - key: evt.Key, - value: user, - }) - case domain.UserAPIKeyChanged: - for _, u := range s.users { - if u.id == evt.ID { - u.key = evt.Key - *u.value = *user - break - } - } - default: - for _, u := range s.users { - if u.id == user.ID() { - *u.value = *user - break - } - } - } - } - } - - return nil -} diff --git a/internal/deployment/app/cleanup_app/cleanup_app_test.go b/internal/deployment/app/cleanup_app/cleanup_app_test.go index b93738c5..263a8b43 100644 --- a/internal/deployment/app/cleanup_app/cleanup_app_test.go +++ b/internal/deployment/app/cleanup_app/cleanup_app_test.go @@ -2,60 +2,95 @@ package cleanup_app_test import ( "context" + "errors" "testing" "time" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/cleanup_app" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" - "github.com/YuukanOO/seelf/internal/deployment/infra/source/raw" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) -type initialData struct { - deployments []*domain.Deployment - targets []*domain.Target -} - func Test_CleanupApp(t *testing.T) { - ctx := context.Background() - sut := func(data initialData) (bus.RequestHandler[bus.UnitType, cleanup_app.Command], *dummyProvider) { - targetsStore := memory.NewTargetsStore(data.targets...) - deploymentsStore := memory.NewDeploymentsStore(data.deployments...) - provider := &dummyProvider{} - return cleanup_app.Handler(targetsStore, deploymentsStore, provider), provider + arrange := func(tb testing.TB, provider domain.Provider, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[bus.UnitType, cleanup_app.Command], + context.Context, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return cleanup_app.Handler(context.TargetsStore, context.DeploymentsStore, provider), context.Context } t.Run("should fail silently if the target does not exist anymore", func(t *testing.T) { - uc, provider := sut(initialData{}) + var provider mockProvider + handler, ctx := arrange(t, &provider) - r, err := uc(ctx, cleanup_app.Command{}) + r, err := handler(ctx, cleanup_app.Command{}) - testutil.IsNil(t, err) - testutil.Equals(t, bus.Unit, r) - testutil.IsFalse(t, provider.called) + assert.Nil(t, err) + assert.Equal(t, bus.Unit, r) + assert.False(t, provider.called) }) - t.Run("should fail if the target is configuring", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "uid")) - deployment := must.Panic(app.NewDeployment(1, raw.Data(""), domain.Production, "uid")) - deployment.HasStarted() - deployment.HasEnded(domain.Services{}, nil) - - uc, provider := sut(initialData{ - targets: []*domain.Target{&target}, - deployments: []*domain.Deployment{&deployment}, + t.Run("should fail if at least one deployment is running", func(t *testing.T) { + var provider mockProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App(fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + )) + deployment := fixture.Deployment(fixture.FromApp(app), + fixture.ForEnvironment(domain.Production), + fixture.WithDeploymentRequestedBy(user.ID())) + assert.Nil(t, deployment.HasStarted()) + + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + _, err := handler(ctx, cleanup_app.Command{ + TargetID: string(target.ID()), + AppID: string(app.ID()), + Environment: string(domain.Production), + From: deployment.Requested().At().Add(-1 * time.Hour), + To: deployment.Requested().At().Add(1 * time.Hour), }) - _, err := uc(ctx, cleanup_app.Command{ + assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err) + assert.False(t, provider.called) + }) + + t.Run("should fail if the target is configuring and at least one successful deployment has been made", func(t *testing.T) { + var provider mockProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App(fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + )) + deployment := fixture.Deployment(fixture.FromApp(app), + fixture.ForEnvironment(domain.Production), + fixture.WithDeploymentRequestedBy(user.ID())) + assert.Nil(t, deployment.HasStarted()) + assert.Nil(t, deployment.HasEnded(domain.Services{}, nil)) + + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + _, err := handler(ctx, cleanup_app.Command{ TargetID: string(target.ID()), AppID: string(app.ID()), Environment: string(domain.Production), @@ -63,65 +98,107 @@ func Test_CleanupApp(t *testing.T) { To: deployment.Requested().At().Add(1 * time.Hour), }) - testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) - testutil.IsFalse(t, provider.called) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) + assert.False(t, provider.called) }) t.Run("should succeed if the target is being deleted", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) + var provider mockProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) target.Configured(target.CurrentVersion(), nil, nil) - target.RequestCleanup(false, "uid") + assert.Nil(t, target.RequestCleanup(false, "uid")) - uc, provider := sut(initialData{ - targets: []*domain.Target{&target}, - }) + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) - _, err := uc(ctx, cleanup_app.Command{ + _, err := handler(ctx, cleanup_app.Command{ TargetID: string(target.ID()), }) - testutil.IsNil(t, err) - testutil.IsFalse(t, provider.called) + assert.Nil(t, err) + assert.False(t, provider.called) }) t.Run("should succeed if no successful deployments has been made", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) - target.Configured(target.CurrentVersion(), nil, nil) + var provider mockProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) - uc, provider := sut(initialData{ - targets: []*domain.Target{&target}, - }) + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) - _, err := uc(ctx, cleanup_app.Command{ + _, err := handler(ctx, cleanup_app.Command{ TargetID: string(target.ID()), }) - testutil.IsNil(t, err) - testutil.IsFalse(t, provider.called) + assert.Nil(t, err) + assert.False(t, provider.called) }) - t.Run("should succeed if the target is ready and successful deployments have been made", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) - target.Configured(target.CurrentVersion(), nil, nil) - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "uid")) - deployment := must.Panic(app.NewDeployment(1, raw.Data(""), domain.Production, "uid")) - deployment.HasStarted() - deployment.HasEnded(domain.Services{}, nil) - - uc, provider := sut(initialData{ - targets: []*domain.Target{&target}, - deployments: []*domain.Deployment{&deployment}, + t.Run("should fail if the target is not ready and a successful deployment has been made", func(t *testing.T) { + var provider mockProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed")) + app := fixture.App(fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + )) + deployment := fixture.Deployment(fixture.FromApp(app), + fixture.ForEnvironment(domain.Production), + fixture.WithDeploymentRequestedBy(user.ID())) + assert.Nil(t, deployment.HasStarted()) + assert.Nil(t, deployment.HasEnded(domain.Services{}, nil)) + + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + _, err := handler(ctx, cleanup_app.Command{ + TargetID: string(target.ID()), + AppID: string(app.ID()), + Environment: string(domain.Production), + From: deployment.Requested().At().Add(-1 * time.Hour), + To: deployment.Requested().At().Add(1 * time.Hour), }) - _, err := uc(ctx, cleanup_app.Command{ + assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err) + assert.False(t, provider.called) + }) + + t.Run("should succeed if the target is ready and successful deployments have been made", func(t *testing.T) { + var provider mockProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + target.Configured(target.CurrentVersion(), nil, nil) + app := fixture.App(fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + )) + deployment := fixture.Deployment(fixture.FromApp(app), + fixture.ForEnvironment(domain.Production), + fixture.WithDeploymentRequestedBy(user.ID())) + assert.Nil(t, deployment.HasStarted()) + assert.Nil(t, deployment.HasEnded(domain.Services{}, nil)) + + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + _, err := handler(ctx, cleanup_app.Command{ TargetID: string(target.ID()), AppID: string(app.ID()), Environment: string(domain.Production), @@ -129,17 +206,17 @@ func Test_CleanupApp(t *testing.T) { To: deployment.Requested().At().Add(1 * time.Hour), }) - testutil.IsNil(t, err) - testutil.IsTrue(t, provider.called) + assert.Nil(t, err) + assert.True(t, provider.called) }) } -type dummyProvider struct { +type mockProvider struct { domain.Provider called bool } -func (d *dummyProvider) Cleanup(_ context.Context, _ domain.AppID, _ domain.Target, _ domain.Environment, s domain.CleanupStrategy) error { +func (d *mockProvider) Cleanup(_ context.Context, _ domain.AppID, _ domain.Target, _ domain.Environment, s domain.CleanupStrategy) error { d.called = s != domain.CleanupStrategySkip return nil } diff --git a/internal/deployment/app/cleanup_app/on_app_cleanup_requested.go b/internal/deployment/app/cleanup_app/on_app_cleanup_requested.go index 2dc5043c..2ba7aee5 100644 --- a/internal/deployment/app/cleanup_app/on_app_cleanup_requested.go +++ b/internal/deployment/app/cleanup_app/on_app_cleanup_requested.go @@ -13,15 +13,13 @@ func OnAppCleanupRequestedHandler(scheduler bus.Scheduler) bus.SignalHandler[dom return func(ctx context.Context, evt domain.AppCleanupRequested) error { now := time.Now().UTC() - err := scheduler.Queue(ctx, Command{ + if err := scheduler.Queue(ctx, Command{ AppID: string(evt.ID), Environment: string(domain.Production), TargetID: string(evt.ProductionConfig.Target()), From: evt.ProductionConfig.Version(), To: now, - }, bus.WithPolicy(bus.JobPolicyCancellable)) - - if err != nil { + }, bus.WithPolicy(bus.JobPolicyCancellable)); err != nil { return err } diff --git a/internal/deployment/app/cleanup_target/cleanup_target_test.go b/internal/deployment/app/cleanup_target/cleanup_target_test.go index 19e6396a..a0af545e 100644 --- a/internal/deployment/app/cleanup_target/cleanup_target_test.go +++ b/internal/deployment/app/cleanup_target/cleanup_target_test.go @@ -5,61 +5,133 @@ import ( "errors" "testing" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/cleanup_target" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_CleanupTarget(t *testing.T) { - sut := func(existingTargets ...*domain.Target) (bus.RequestHandler[bus.UnitType, cleanup_target.Command], *dummyProvider) { - targetsStore := memory.NewTargetsStore(existingTargets...) - deploymentsStore := memory.NewDeploymentsStore() - provider := &dummyProvider{} - return cleanup_target.Handler(targetsStore, deploymentsStore, provider), provider + + arrange := func(tb testing.TB, provider domain.Provider, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[bus.UnitType, cleanup_target.Command], + context.Context, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return cleanup_target.Handler(context.TargetsStore, context.DeploymentsStore, provider), context.Context } t.Run("should silently fail if the target does not exist anymore", func(t *testing.T) { - uc, provider := sut() + var provider dummyProvider + handler, ctx := arrange(t, &provider) - _, err := uc(context.Background(), cleanup_target.Command{}) + _, err := handler(ctx, cleanup_target.Command{}) - testutil.IsNil(t, err) - testutil.IsFalse(t, provider.called) + assert.Nil(t, err) + assert.False(t, provider.called) }) - t.Run("should skip the provider cleanup if the target is not reachable", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) - target.Configured(target.CurrentVersion(), nil, errors.New("some error")) + t.Run("should skip the cleanup if the target has never been configured correctly", func(t *testing.T) { + var provider dummyProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + target.Configured(target.CurrentVersion(), nil, errors.New("configuration_failed")) + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) + + _, err := handler(ctx, cleanup_target.Command{ + ID: string(target.ID()), + }) - uc, provider := sut(&target) + assert.Nil(t, err) + assert.False(t, provider.called) + }) - _, err := uc(context.Background(), cleanup_target.Command{ + t.Run("should fail if a deployment is running on this target", func(t *testing.T) { + var provider dummyProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App(fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + )) + deployment := fixture.Deployment(fixture.FromApp(app), + fixture.ForEnvironment(domain.Production), + fixture.WithDeploymentRequestedBy(user.ID())) + assert.Nil(t, deployment.HasStarted()) + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + _, err := handler(ctx, cleanup_target.Command{ ID: string(target.ID()), }) - testutil.IsNil(t, err) - testutil.IsFalse(t, provider.called) + assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err) + assert.False(t, provider.called) }) - t.Run("should succeed if the target can be safely deleted", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) + t.Run("should fail if being configured", func(t *testing.T) { + var provider dummyProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) + + _, err := handler(ctx, cleanup_target.Command{ + ID: string(target.ID()), + }) + + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) + assert.False(t, provider.called) + }) + + t.Run("should fail if has been configured in the past but is now unreachable", func(t *testing.T) { + var provider dummyProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) target.Configured(target.CurrentVersion(), nil, nil) + assert.Nil(t, target.Reconfigure()) + target.Configured(target.CurrentVersion(), nil, errors.New("configuration_failed")) + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) + + _, err := handler(ctx, cleanup_target.Command{ + ID: string(target.ID()), + }) - uc, provider := sut(&target) + assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err) + assert.False(t, provider.called) + }) + + t.Run("should cleanup the target if it is correctly configured", func(t *testing.T) { + var provider dummyProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + target.Configured(target.CurrentVersion(), nil, nil) + handler, ctx := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) - _, err := uc(context.Background(), cleanup_target.Command{ + _, err := handler(ctx, cleanup_target.Command{ ID: string(target.ID()), }) - testutil.IsNil(t, err) - testutil.IsTrue(t, provider.called) + assert.Nil(t, err) + assert.True(t, provider.called) }) } diff --git a/internal/deployment/app/configure_target/configure_target_test.go b/internal/deployment/app/configure_target/configure_target_test.go index b0bfafcd..c7b85db0 100644 --- a/internal/deployment/app/configure_target/configure_target_test.go +++ b/internal/deployment/app/configure_target/configure_target_test.go @@ -6,83 +6,97 @@ import ( "testing" "time" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/configure_target" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" ) func Test_ConfigureTarget(t *testing.T) { - sut := func(existingTargets ...*domain.Target) (bus.RequestHandler[bus.UnitType, configure_target.Command], *dummyProvider) { - provider := &dummyProvider{} - store := memory.NewTargetsStore(existingTargets...) - return configure_target.Handler(store, store, provider), provider + + arrange := func(tb testing.TB, provider domain.Provider, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[bus.UnitType, configure_target.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return configure_target.Handler(context.TargetsStore, context.TargetsStore, provider), context.Dispatcher } t.Run("should fail silently if the target is not found", func(t *testing.T) { - uc, provider := sut() + var provider dummyProvider + handler, _ := arrange(t, &provider) - _, err := uc(context.Background(), configure_target.Command{}) + _, err := handler(context.Background(), configure_target.Command{}) - testutil.IsNil(t, err) - testutil.IsFalse(t, provider.called) + assert.Nil(t, err) + assert.False(t, provider.called) }) t.Run("should returns early if the version is outdated", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) - created := testutil.EventIs[domain.TargetCreated](t, &target, 0) - uc, provider := sut(&target) - - _, err := uc(context.Background(), configure_target.Command{ + var provider dummyProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + handler, dispatcher := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) + + _, err := handler(context.Background(), configure_target.Command{ ID: string(target.ID()), - Version: created.State.Version().Add(-1 * time.Second), + Version: target.CurrentVersion().Add(-1 * time.Second), }) - testutil.IsNil(t, err) - testutil.IsFalse(t, provider.called) + assert.Nil(t, err) + assert.HasLength(t, 0, dispatcher.Signals()) + assert.False(t, provider.called) }) t.Run("should correctly mark the target as failed if the provider fails", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) - created := testutil.EventIs[domain.TargetCreated](t, &target, 0) - uc, provider := sut(&target) providerErr := errors.New("some error") - provider.err = providerErr - - _, err := uc(context.Background(), configure_target.Command{ + provider := dummyProvider{err: providerErr} + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + handler, dispatcher := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) + + _, err := handler(context.Background(), configure_target.Command{ ID: string(target.ID()), - Version: created.State.Version(), + Version: target.CurrentVersion(), }) - testutil.IsNil(t, err) - testutil.IsTrue(t, provider.called) - evt := testutil.EventIs[domain.TargetStateChanged](t, &target, 1) - testutil.Equals(t, domain.TargetStatusFailed, evt.State.Status()) - testutil.Equals(t, providerErr.Error(), evt.State.ErrCode().MustGet()) + assert.Nil(t, err) + assert.True(t, provider.called) + assert.HasLength(t, 1, dispatcher.Signals()) + changed := assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.TargetStatusFailed, changed.State.Status()) + assert.Equal(t, providerErr.Error(), changed.State.ErrCode().MustGet()) }) t.Run("should correctly mark the target as configured if everything is good", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) - created := testutil.EventIs[domain.TargetCreated](t, &target, 0) - uc, provider := sut(&target) - - _, err := uc(context.Background(), configure_target.Command{ + var provider dummyProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + handler, dispatcher := arrange(t, &provider, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) + + _, err := handler(context.Background(), configure_target.Command{ ID: string(target.ID()), - Version: created.State.Version(), + Version: target.CurrentVersion(), }) - testutil.IsNil(t, err) - testutil.IsTrue(t, provider.called) - evt := testutil.EventIs[domain.TargetStateChanged](t, &target, 1) - testutil.Equals(t, domain.TargetStatusReady, evt.State.Status()) + assert.Nil(t, err) + assert.True(t, provider.called) + assert.HasLength(t, 1, dispatcher.Signals()) + changed := assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.TargetStatusReady, changed.State.Status()) + assert.Equal(t, target.CurrentVersion(), changed.State.LastReadyVersion().MustGet()) }) } diff --git a/internal/deployment/app/configure_target/on_app_cleanup_requested.go b/internal/deployment/app/configure_target/on_app_cleanup_requested.go index b31141c2..273cb2a7 100644 --- a/internal/deployment/app/configure_target/on_app_cleanup_requested.go +++ b/internal/deployment/app/configure_target/on_app_cleanup_requested.go @@ -7,40 +7,38 @@ import ( "github.com/YuukanOO/seelf/pkg/bus" ) -// When an application cleanup has been requested, unexpose the application from all targets. +// When an application cleanup has been requested, un-expose the application from all targets. func OnAppCleanupRequestedHandler( reader domain.TargetsReader, writer domain.TargetsWriter, ) bus.SignalHandler[domain.AppCleanupRequested] { return func(ctx context.Context, evt domain.AppCleanupRequested) error { - if evt.ProductionConfig.Target() == evt.StagingConfig.Target() { - target, err := reader.GetByID(ctx, evt.ProductionConfig.Target()) - - if err != nil { - return err - } - - target.UnExposeEntrypoints(evt.ID) - - return writer.Write(ctx, &target) + if err := unExpose(ctx, reader, writer, evt.ProductionConfig.Target(), evt.ID); err != nil { + return err } - productionTarget, err := reader.GetByID(ctx, evt.ProductionConfig.Target()) - - if err != nil { - return err + if evt.ProductionConfig.Target() == evt.StagingConfig.Target() { + return nil } - productionTarget.UnExposeEntrypoints(evt.ID, domain.Production) + return unExpose(ctx, reader, writer, evt.StagingConfig.Target(), evt.ID) + } +} - stagingTarget, err := reader.GetByID(ctx, evt.StagingConfig.Target()) +func unExpose( + ctx context.Context, + reader domain.TargetsReader, + writer domain.TargetsWriter, + id domain.TargetID, + app domain.AppID, +) error { + target, err := reader.GetByID(ctx, id) - if err != nil { - return err - } + if err != nil { + return err + } - stagingTarget.UnExposeEntrypoints(evt.ID, domain.Staging) + target.UnExposeEntrypoints(app) - return writer.Write(ctx, &productionTarget, &stagingTarget) - } + return writer.Write(ctx, &target) } diff --git a/internal/deployment/app/configure_target/on_deployment_state_changed.go b/internal/deployment/app/configure_target/on_deployment_state_changed.go index 4de3ad16..012269c5 100644 --- a/internal/deployment/app/configure_target/on_deployment_state_changed.go +++ b/internal/deployment/app/configure_target/on_deployment_state_changed.go @@ -22,7 +22,7 @@ func OnDeploymentStateChangedHandler( return err } - target.ExposeEntrypoints(evt.ID.AppID(), evt.Config.Environment(), evt.State.Services().Get(domain.Services{})) + target.ExposeEntrypoints(evt.ID.AppID(), evt.Config.Environment(), evt.State.Services().Get(nil)) return writer.Write(ctx, &target) } diff --git a/internal/deployment/app/create_app/create_app.go b/internal/deployment/app/create_app/create_app.go index 322a7ba9..235a04b7 100644 --- a/internal/deployment/app/create_app/create_app.go +++ b/internal/deployment/app/create_app/create_app.go @@ -102,7 +102,7 @@ func Handler( vcs.Authenticated(token) } - app.UseVersionControl(vcs) + _ = app.UseVersionControl(vcs) } if err := writer.Write(ctx, &app); err != nil { diff --git a/internal/deployment/app/create_app/create_app_test.go b/internal/deployment/app/create_app/create_app_test.go index 806dbed7..c63c4aee 100644 --- a/internal/deployment/app/create_app/create_app_test.go +++ b/internal/deployment/app/create_app/create_app_test.go @@ -4,39 +4,81 @@ import ( "context" "testing" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/create_app" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "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/validate" + "github.com/YuukanOO/seelf/pkg/validate/strings" ) func Test_CreateApp(t *testing.T) { - ctx := auth.WithUserID(context.Background(), "some-uid") - sut := func(existingApps ...*domain.App) bus.RequestHandler[string, create_app.Command] { - store := memory.NewAppsStore(existingApps...) - return create_app.Handler(store, store) + + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[string, create_app.Command], + context.Context, + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return create_app.Handler(context.AppsStore, context.AppsStore), context.Context, context.Dispatcher } t.Run("should require valid inputs", func(t *testing.T) { - uc := sut() - id, err := uc(ctx, create_app.Command{}) + handler, ctx, _ := arrange(t) + + id, err := handler(ctx, create_app.Command{}) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - testutil.Equals(t, "", id) + assert.Zero(t, id) + assert.ValidationError(t, validate.FieldErrors{ + "name": domain.ErrInvalidAppName, + "production.target": strings.ErrRequired, + "staging.target": strings.ErrRequired, + }, err) }) t.Run("should fail if the name is already taken", func(t *testing.T) { - a := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true), "uid")) - uc := sut(&a) + user := authfixture.User() + productionTarget := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + stagingTarget := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + existingApp := fixture.App(fixture.WithAppName("my-app"), + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(productionTarget.ID()), + domain.NewEnvironmentConfig(stagingTarget.ID()), + )) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&productionTarget, &stagingTarget), + fixture.WithApps(&existingApp), + ) - id, err := uc(ctx, create_app.Command{ + id, err := handler(ctx, create_app.Command{ + Name: "my-app", + Production: create_app.EnvironmentConfig{ + Target: string(productionTarget.ID()), + }, + Staging: create_app.EnvironmentConfig{ + Target: string(stagingTarget.ID()), + }, + }) + + assert.Zero(t, id) + assert.ValidationError(t, validate.FieldErrors{ + "production.target": domain.ErrAppNameAlreadyTaken, + "staging.target": domain.ErrAppNameAlreadyTaken, + }, err) + }) + + t.Run("should fail if provided targets does not exists", func(t *testing.T) { + handler, ctx, _ := arrange(t) + + id, err := handler(ctx, create_app.Command{ Name: "my-app", Production: create_app.EnvironmentConfig{ Target: "production-target", @@ -46,26 +88,53 @@ func Test_CreateApp(t *testing.T) { }, }) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.Equals(t, "", id) - testutil.ErrorIs(t, domain.ErrAppNameAlreadyTaken, validationErr["production.target"]) - testutil.ErrorIs(t, domain.ErrAppNameAlreadyTaken, validationErr["staging.target"]) + assert.Zero(t, id) + assert.ValidationError(t, validate.FieldErrors{ + "production.target": apperr.ErrNotFound, + "staging.target": apperr.ErrNotFound, + }, err) }) t.Run("should create a new app if everything is good", func(t *testing.T) { - uc := sut() - id, err := uc(ctx, create_app.Command{ + 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_app.Command{ Name: "my-app", Production: create_app.EnvironmentConfig{ - Target: "production-target", + Target: string(target.ID()), }, Staging: create_app.EnvironmentConfig{ - Target: "staging-target", + Target: string(target.ID()), }, + VersionControl: monad.Value(create_app.VersionControl{ + Url: "https://somewhere.git", + Token: monad.Value("some-token"), + }), }) - testutil.IsNil(t, err) - testutil.NotEquals(t, "", id) + assert.Nil(t, err) + assert.NotZero(t, id) + assert.HasLength(t, 2, dispatcher.Signals()) + + created := assert.Is[domain.AppCreated](t, dispatcher.Signals()[0]) + assert.DeepEqual(t, domain.AppCreated{ + ID: domain.AppID(id), + Name: "my-app", + Production: created.Production, + Staging: created.Staging, + Created: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Created.At())), + }, created) + assert.Equal(t, target.ID(), created.Production.Target()) + assert.Equal(t, target.ID(), created.Staging.Target()) + + versionControlConfigured := assert.Is[domain.AppVersionControlConfigured](t, dispatcher.Signals()[1]) + assert.Equal(t, created.ID, versionControlConfigured.ID) + assert.Equal(t, "https://somewhere.git", versionControlConfigured.Config.Url().String()) + assert.Equal(t, "some-token", versionControlConfigured.Config.Token().Get("")) }) } diff --git a/internal/deployment/app/create_registry/create_registry_test.go b/internal/deployment/app/create_registry/create_registry_test.go index 47fca249..ce74d319 100644 --- a/internal/deployment/app/create_registry/create_registry_test.go +++ b/internal/deployment/app/create_registry/create_registry_test.go @@ -4,52 +4,70 @@ import ( "context" "testing" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/create_registry" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" - "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" "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/testutil" "github.com/YuukanOO/seelf/pkg/validate" + "github.com/YuukanOO/seelf/pkg/validate/strings" ) func Test_CreateRegistry(t *testing.T) { - ctx := auth.WithUserID(context.Background(), "some-uid") - sut := func(existing ...*domain.Registry) bus.RequestHandler[string, create_registry.Command] { - store := memory.NewRegistriesStore(existing...) - return create_registry.Handler(store, store) + + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[string, create_registry.Command], + context.Context, + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return create_registry.Handler(context.RegistriesStore, context.RegistriesStore), context.Context, context.Dispatcher } t.Run("should require valid inputs", func(t *testing.T) { - uc := sut() - id, err := uc(ctx, create_registry.Command{}) + handler, ctx, _ := arrange(t) + + id, err := handler(ctx, create_registry.Command{}) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - testutil.Equals(t, "", id) + assert.Zero(t, id) + assert.ValidationError(t, validate.FieldErrors{ + "name": strings.ErrRequired, + "url": domain.ErrInvalidUrl, + }, err) }) t.Run("should fail if the url is already taken", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) - uc := sut(&r) + user := authfixture.User() + registry := fixture.Registry( + fixture.WithRegistryCreatedBy(user.ID()), + fixture.WithUrl(must.Panic(domain.UrlFrom("http://example.com"))), + ) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithRegistries(®istry), + ) - id, err := uc(ctx, create_registry.Command{ + id, err := handler(ctx, create_registry.Command{ Name: "registry", Url: "http://example.com", }) - testutil.Equals(t, "", id) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, validationErr["url"]) + assert.Zero(t, id) + assert.ValidationError(t, validate.FieldErrors{ + "url": domain.ErrUrlAlreadyTaken, + }, err) }) t.Run("should create a new registry if everything is good", func(t *testing.T) { - uc := sut() + user := authfixture.User() + handler, ctx, dispatcher := arrange(t, fixture.WithUsers(&user)) - id, err := uc(ctx, create_registry.Command{ + id, err := handler(ctx, create_registry.Command{ Name: "registry", Url: "http://example.com", Credentials: monad.Value(create_registry.Credentials{ @@ -58,7 +76,22 @@ func Test_CreateRegistry(t *testing.T) { }), }) - testutil.NotEquals(t, "", id) - testutil.IsNil(t, err) + assert.NotZero(t, id) + assert.Nil(t, err) + assert.HasLength(t, 2, dispatcher.Signals()) + + created := assert.Is[domain.RegistryCreated](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.RegistryCreated{ + ID: domain.RegistryID(id), + Name: "registry", + Url: must.Panic(domain.UrlFrom("http://example.com")), + Created: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Created.At())), + }, created) + + credentialsSet := assert.Is[domain.RegistryCredentialsChanged](t, dispatcher.Signals()[1]) + assert.Equal(t, domain.RegistryCredentialsChanged{ + ID: domain.RegistryID(id), + Credentials: domain.NewCredentials("user", "password"), + }, credentialsSet) }) } 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 735c7365..02ce76fe 100644 --- a/internal/deployment/app/create_target/create_target_test.go +++ b/internal/deployment/app/create_target/create_target_test.go @@ -4,119 +4,141 @@ import ( "context" "testing" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/create_target" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" - "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" "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/testutil" "github.com/YuukanOO/seelf/pkg/validate" + "github.com/YuukanOO/seelf/pkg/validate/strings" ) func Test_CreateTarget(t *testing.T) { - var ( - uid auth.UserID = "uid" - ctx = auth.WithUserID(context.Background(), uid) - config dummyConfig - ) - sut := func(existingTargets ...*domain.Target) bus.RequestHandler[string, create_target.Command] { - store := memory.NewTargetsStore(existingTargets...) - - return create_target.Handler(store, store, &dummyProvider{}) + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[string, create_target.Command], + context.Context, + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return create_target.Handler(context.TargetsStore, context.TargetsStore, &dummyProvider{}), context.Context, context.Dispatcher } t.Run("should require valid inputs", func(t *testing.T) { - uc := sut() + handler, ctx, _ := arrange(t) - _, err := uc(ctx, create_target.Command{}) + _, err := handler(ctx, create_target.Command{}) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) + assert.ValidationError(t, validate.FieldErrors{ + "name": strings.ErrRequired, + }, err) }) - t.Run("should require a unique url", func(t *testing.T) { - target := must.Panic(domain.NewTarget("target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), - domain.NewProviderConfigRequirement(config, true), uid)) + t.Run("should require a unique url and config", func(t *testing.T) { + var config = fixture.ProviderConfig() + user := authfixture.User() + target := fixture.Target( + fixture.WithTargetCreatedBy(user.ID()), + fixture.WithProviderConfig(config), + ) + assert.Nil(t, target.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true))) - uc := sut(&target) + handler, ctx, _ := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target)) - _, err := uc(ctx, create_target.Command{ + _, err := handler(ctx, create_target.Command{ Name: "target", - Url: "http://example.com", + Url: monad.Value("http://example.com"), Provider: config, }) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - validateError, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, validateError["url"]) - testutil.ErrorIs(t, domain.ErrConfigAlreadyTaken, validateError[config.Kind()]) + assert.ValidationError(t, validate.FieldErrors{ + "url": domain.ErrUrlAlreadyTaken, + config.Kind(): domain.ErrConfigAlreadyTaken, + }, err) }) t.Run("should require a valid provider config", func(t *testing.T) { - uc := sut() + handler, ctx, _ := arrange(t) - _, err := uc(ctx, create_target.Command{ + _, err := handler(ctx, create_target.Command{ Name: "target", - Url: "http://example.com", }) - testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err) + assert.ErrorIs(t, domain.ErrNoValidProviderFound, err) }) - t.Run("should require a unique provider config", func(t *testing.T) { - target := must.Panic(domain.NewTarget("target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), - domain.NewProviderConfigRequirement(config, true), uid)) + 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)) - uc := sut(&target) + id, err := handler(ctx, create_target.Command{ + Name: "target-one", + Provider: fixture.ProviderConfig(), + }) - _, err := uc(ctx, create_target.Command{ - Name: "target", - Url: "http://another.example.com", - Provider: config, + assert.Nil(t, err) + assert.NotZero(t, id) + + id, err = handler(ctx, create_target.Command{ + Name: "target-two", + Provider: fixture.ProviderConfig(), }) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - validateError, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrConfigAlreadyTaken, validateError[config.Kind()]) + 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) { - uc := sut() + var config = fixture.ProviderConfig() + user := authfixture.User() + handler, ctx, dispatcher := arrange(t, fixture.WithUsers(&user)) - id, err := uc(ctx, create_target.Command{ + id, err := handler(ctx, create_target.Command{ Name: "target", - Url: "http://example.com", + Url: monad.Value("http://example.com"), Provider: config, }) - testutil.IsNil(t, err) - testutil.NotEquals(t, "", id) + assert.Nil(t, err) + assert.NotZero(t, id) + 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", + 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]) }) } -type ( - dummyProvider struct { - domain.Provider - } - - dummyConfig struct{} -) +type dummyProvider struct { + domain.Provider +} func (*dummyProvider) Prepare(ctx context.Context, payload any, existing ...domain.ProviderConfig) (domain.ProviderConfig, error) { if payload == nil { return nil, domain.ErrNoValidProviderFound } - return dummyConfig{}, nil + return payload.(domain.ProviderConfig), nil } - -func (dummyConfig) Fingerprint() string { return "dummy" } -func (c dummyConfig) Equals(o domain.ProviderConfig) bool { return c == o } -func (dummyConfig) Kind() string { return "dummy" } -func (dummyConfig) String() string { return "dummy" } diff --git a/internal/deployment/app/delete_app/delete_app_test.go b/internal/deployment/app/delete_app/delete_app_test.go index 4967c35b..e5adab8b 100644 --- a/internal/deployment/app/delete_app/delete_app_test.go +++ b/internal/deployment/app/delete_app/delete_app_test.go @@ -2,76 +2,95 @@ package delete_app_test import ( "context" - "os" "testing" - "github.com/YuukanOO/seelf/cmd/config" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/delete_app" "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/memory" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" + "github.com/YuukanOO/seelf/pkg/bus/spy" "github.com/YuukanOO/seelf/pkg/log" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) -func DeleteApp(t *testing.T) { - ctx := context.Background() - logger, _ := log.NewLogger() +func Test_DeleteApp(t *testing.T) { - sut := func(initialApps ...*domain.App) bus.RequestHandler[bus.UnitType, delete_app.Command] { - opts := config.Default(config.WithTestDefaults()) - appsStore := memory.NewAppsStore(initialApps...) - artifactManager := artifact.NewLocal(opts, logger) - - t.Cleanup(func() { - os.RemoveAll(opts.DataDir()) - }) - - return delete_app.Handler(appsStore, appsStore, artifactManager) + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[bus.UnitType, delete_app.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + logger, _ := log.NewLogger() + artifactManager := artifact.NewLocal(context.Config, logger) + return delete_app.Handler(context.AppsStore, context.AppsStore, artifactManager), context.Dispatcher } t.Run("should fail silently if the application does not exist anymore", func(t *testing.T) { - uc := sut() + handler, dispatcher := arrange(t) - r, err := uc(ctx, delete_app.Command{ + r, err := handler(context.Background(), delete_app.Command{ ID: "some-id", }) - testutil.IsNil(t, err) - testutil.Equals(t, bus.Unit, r) + assert.Nil(t, err) + assert.Equal(t, bus.Unit, r) + assert.HasLength(t, 0, dispatcher.Signals()) }) - t.Run("should fail if the application cleanup has not been requested", func(t *testing.T) { - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "uid")) - uc := sut(&app) - - r, err := uc(ctx, delete_app.Command{ + t.Run("should fail if the application cleanup has not been requested first", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + handler, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + r, err := handler(context.Background(), delete_app.Command{ ID: string(app.ID()), }) - testutil.ErrorIs(t, domain.ErrAppCleanupNeeded, err) - testutil.Equals(t, bus.Unit, r) + assert.ErrorIs(t, domain.ErrAppCleanupNeeded, err) + assert.Equal(t, bus.Unit, r) }) t.Run("should succeed if everything is good", func(t *testing.T) { - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "uid")) - app.RequestCleanup("uid") - - uc := sut(&app) - - r, err := uc(ctx, delete_app.Command{ + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + app.RequestCleanup(user.ID()) + handler, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + r, err := handler(context.Background(), delete_app.Command{ ID: string(app.ID()), }) - testutil.IsNil(t, err) - testutil.Equals(t, bus.Unit, r) - testutil.HasNEvents(t, &app, 3) - testutil.EventIs[domain.AppDeleted](t, &app, 2) + assert.Nil(t, err) + assert.Equal(t, bus.Unit, r) + assert.HasLength(t, 1, dispatcher.Signals()) + + deleted := assert.Is[domain.AppDeleted](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.AppDeleted{ + ID: app.ID(), + }, deleted) }) } diff --git a/internal/deployment/app/delete_registry/delete_registry_test.go b/internal/deployment/app/delete_registry/delete_registry_test.go index f90ad4fa..8d881612 100644 --- a/internal/deployment/app/delete_registry/delete_registry_test.go +++ b/internal/deployment/app/delete_registry/delete_registry_test.go @@ -4,41 +4,51 @@ import ( "context" "testing" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/delete_registry" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" ) func Test_DeleteRegistry(t *testing.T) { - sut := func(existing ...*domain.Registry) bus.RequestHandler[bus.UnitType, delete_registry.Command] { - store := memory.NewRegistriesStore(existing...) - return delete_registry.Handler(store, store) + + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[bus.UnitType, delete_registry.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return delete_registry.Handler(context.RegistriesStore, context.RegistriesStore), context.Dispatcher } t.Run("should require an existing registry", func(t *testing.T) { - uc := sut() + handler, _ := arrange(t) - _, err := uc(context.Background(), delete_registry.Command{ + _, err := handler(context.Background(), delete_registry.Command{ ID: "non-existing-id", }) - testutil.ErrorIs(t, apperr.ErrNotFound, err) + assert.ErrorIs(t, apperr.ErrNotFound, err) }) t.Run("should delete the registry", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) - uc := sut(&r) + user := authfixture.User() + registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID())) + handler, dispatcher := arrange(t, fixture.WithUsers(&user), fixture.WithRegistries(®istry)) - _, err := uc(context.Background(), delete_registry.Command{ - ID: string(r.ID()), + _, err := handler(context.Background(), delete_registry.Command{ + ID: string(registry.ID()), }) - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.RegistryDeleted](t, &r, 1) - testutil.Equals(t, r.ID(), evt.ID) + assert.Nil(t, err) + assert.HasLength(t, 1, dispatcher.Signals()) + + deleted := assert.Is[domain.RegistryDeleted](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.RegistryDeleted{ + ID: registry.ID(), + }, deleted) }) } diff --git a/internal/deployment/app/delete_target/delete_target_test.go b/internal/deployment/app/delete_target/delete_target_test.go index 2c00fe9f..ce0f4c32 100644 --- a/internal/deployment/app/delete_target/delete_target_test.go +++ b/internal/deployment/app/delete_target/delete_target_test.go @@ -4,62 +4,71 @@ import ( "context" "testing" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/delete_target" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" ) func Test_DeleteTarget(t *testing.T) { - ctx := context.Background() - sut := func(existingTargets ...*domain.Target) (bus.RequestHandler[bus.UnitType, delete_target.Command], *dummyProvider) { - targetsStore := memory.NewTargetsStore(existingTargets...) - provider := &dummyProvider{} - return delete_target.Handler(targetsStore, targetsStore, provider), provider + arrange := func(tb testing.TB, provider domain.Provider, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[bus.UnitType, delete_target.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return delete_target.Handler(context.TargetsStore, context.TargetsStore, provider), context.Dispatcher } t.Run("should fail silently if the target does not exist anymore", func(t *testing.T) { - uc, provider := sut() + var provider dummyProvider + handler, dispatcher := arrange(t, &provider) - _, err := uc(ctx, delete_target.Command{}) + _, err := handler(context.Background(), delete_target.Command{}) - testutil.IsNil(t, err) - testutil.IsFalse(t, provider.called) + assert.Nil(t, err) + assert.False(t, provider.called) + assert.HasLength(t, 0, dispatcher.Signals()) }) t.Run("should fail if the target has not been requested for cleanup", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) + var provider dummyProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + handler, dispatcher := arrange(t, &provider, fixture.WithUsers(&user), fixture.WithTargets(&target)) - uc, provider := sut(&target) - - _, err := uc(ctx, delete_target.Command{ + _, err := handler(context.Background(), delete_target.Command{ ID: string(target.ID()), }) - testutil.ErrorIs(t, domain.ErrTargetCleanupNeeded, err) - testutil.IsFalse(t, provider.called) + assert.ErrorIs(t, domain.ErrTargetCleanupNeeded, err) + assert.False(t, provider.called) + assert.HasLength(t, 0, dispatcher.Signals()) }) t.Run("should succeed if everything is good", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) + var provider dummyProvider + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, "uid")) - - uc, provider := sut(&target) + assert.Nil(t, target.RequestCleanup(false, user.ID())) + handler, dispatcher := arrange(t, &provider, fixture.WithUsers(&user), fixture.WithTargets(&target)) - _, err := uc(ctx, delete_target.Command{ + _, err := handler(context.Background(), delete_target.Command{ ID: string(target.ID()), }) - testutil.IsNil(t, err) - testutil.IsTrue(t, provider.called) + assert.Nil(t, err) + assert.True(t, provider.called) + assert.HasLength(t, 1, dispatcher.Signals()) + + deleted := assert.Is[domain.TargetDeleted](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.TargetDeleted{ + ID: target.ID(), + }, deleted) }) } diff --git a/internal/deployment/app/deploy/deploy_test.go b/internal/deployment/app/deploy/deploy_test.go index 52fef77f..37ef978b 100644 --- a/internal/deployment/app/deploy/deploy_test.go +++ b/internal/deployment/app/deploy/deploy_test.go @@ -3,174 +3,228 @@ package deploy_test import ( "context" "errors" - "os" "testing" - "github.com/YuukanOO/seelf/cmd/config" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/deploy" "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/memory" "github.com/YuukanOO/seelf/internal/deployment/infra/source/raw" - "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" + "github.com/YuukanOO/seelf/pkg/bus/spy" "github.com/YuukanOO/seelf/pkg/log" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) -type initialData struct { - deployments []*domain.Deployment - targets []*domain.Target -} - func Test_Deploy(t *testing.T) { - ctx := auth.WithUserID(context.Background(), "some-uid") - logger, _ := log.NewLogger() - sut := func( + arrange := func( + tb testing.TB, source domain.Source, provider domain.Provider, - data initialData, - ) bus.RequestHandler[bus.UnitType, deploy.Command] { - opts := config.Default(config.WithTestDefaults()) - store := memory.NewDeploymentsStore(data.deployments...) - targetsStore := memory.NewTargetsStore(data.targets...) - registriesStore := memory.NewRegistriesStore() - artifactManager := artifact.NewLocal(opts, logger) - - t.Cleanup(func() { - os.RemoveAll(opts.DataDir()) - }) - - return deploy.Handler(store, store, artifactManager, source, provider, targetsStore, registriesStore) + seed ...fixture.SeedBuilder, + ) ( + bus.RequestHandler[bus.UnitType, deploy.Command], + context.Context, + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + logger, _ := log.NewLogger() + artifactManager := artifact.NewLocal(context.Config, logger) + return deploy.Handler(context.DeploymentsStore, context.DeploymentsStore, artifactManager, source, provider, context.TargetsStore, context.RegistriesStore), context.Context, context.Dispatcher } t.Run("should fail silently if the deployment does not exists", func(t *testing.T) { - uc := sut(source(nil), provider(nil), initialData{}) - r, err := uc(ctx, deploy.Command{}) - - testutil.IsNil(t, err) - testutil.Equals(t, bus.Unit, r) - }) + handler, ctx, _ := arrange(t, source(nil), provider(nil)) - t.Run("should mark the deployment has failed if the target does not exist anymore", func(t *testing.T) { - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid")) - src := source(nil) - meta := must.Panic(src.Prepare(ctx, app, 42)) - depl := must.Panic(app.NewDeployment(1, meta, domain.Production, "some-uid")) + r, err := handler(ctx, deploy.Command{}) - uc := sut(src, provider(nil), initialData{ - deployments: []*domain.Deployment{&depl}, - }) + assert.Nil(t, err) + assert.Equal(t, bus.Unit, r) + }) - _, err := uc(ctx, deploy.Command{ - AppID: string(depl.ID().AppID()), - DeploymentNumber: int(depl.ID().DeploymentNumber()), + t.Run("should mark the deployment has failed if the target is configuring", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + deployment := fixture.Deployment( + fixture.WithDeploymentRequestedBy(user.ID()), + fixture.FromApp(app), + ) + handler, ctx, dispatcher := arrange(t, source(nil), provider(nil), + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + _, err := handler(ctx, deploy.Command{ + AppID: string(deployment.ID().AppID()), + DeploymentNumber: int(deployment.ID().DeploymentNumber()), }) - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.DeploymentStateChanged](t, &depl, 2) - testutil.Equals(t, apperr.ErrNotFound.Error(), evt.State.ErrCode().MustGet()) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) + assert.HasLength(t, 0, dispatcher.Signals()) }) t.Run("should mark the deployment has failed if source does not succeed", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "some-uid")) + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) target.Configured(target.CurrentVersion(), nil, nil) - - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "some-uid")) - srcErr := errors.New("source_failed") - src := source(srcErr) - meta := must.Panic(src.Prepare(ctx, app, 42)) - depl := must.Panic(app.NewDeployment(1, meta, domain.Production, "some-uid")) - uc := sut(src, provider(nil), initialData{ - deployments: []*domain.Deployment{&depl}, - targets: []*domain.Target{&target}, + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + deployment := fixture.Deployment( + fixture.WithDeploymentRequestedBy(user.ID()), + fixture.FromApp(app), + ) + sourceErr := errors.New("source_failed") + handler, ctx, dispatcher := arrange(t, source(sourceErr), provider(nil), + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + r, err := handler(ctx, deploy.Command{ + AppID: string(deployment.ID().AppID()), + DeploymentNumber: int(deployment.ID().DeploymentNumber()), }) - r, err := uc(ctx, deploy.Command{ - AppID: string(depl.ID().AppID()), - DeploymentNumber: int(depl.ID().DeploymentNumber()), + assert.Nil(t, err) + assert.Equal(t, bus.Unit, r) + + changed := assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.DeploymentStatusRunning, changed.State.Status()) + + changed = assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[1]) + assert.Equal(t, domain.DeploymentStatusFailed, changed.State.Status()) + assert.Equal(t, sourceErr.Error(), changed.State.ErrCode().MustGet()) + }) + + t.Run("should mark the deployment has failed in the target is not correctly configured", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + target.Configured(target.CurrentVersion(), nil, errors.New("target_failed")) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + deployment := fixture.Deployment( + fixture.WithDeploymentRequestedBy(user.ID()), + fixture.FromApp(app), + ) + handler, ctx, dispatcher := arrange(t, source(nil), provider(nil), + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + r, err := handler(ctx, deploy.Command{ + AppID: string(deployment.ID().AppID()), + DeploymentNumber: int(deployment.ID().DeploymentNumber()), }) - testutil.IsNil(t, err) - testutil.Equals(t, bus.Unit, r) + assert.Nil(t, err) + assert.Equal(t, bus.Unit, r) - evt := testutil.EventIs[domain.DeploymentStateChanged](t, &depl, 2) - testutil.IsTrue(t, evt.State.StartedAt().HasValue()) - testutil.IsTrue(t, evt.State.FinishedAt().HasValue()) - testutil.Equals(t, srcErr.Error(), evt.State.ErrCode().MustGet()) - testutil.Equals(t, domain.DeploymentStatusFailed, evt.State.Status()) + changed := assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.DeploymentStatusRunning, changed.State.Status()) + + changed = assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[1]) + assert.Equal(t, domain.DeploymentStatusFailed, changed.State.Status()) + assert.Equal(t, domain.ErrTargetConfigurationFailed.Error(), changed.State.ErrCode().MustGet()) }) t.Run("should mark the deployment has failed if provider does not run the deployment successfully", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "some-uid")) + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) target.Configured(target.CurrentVersion(), nil, nil) - - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "some-uid")) - providerErr := errors.New("run_failed") - be := provider(providerErr) - src := source(nil) - meta := must.Panic(src.Prepare(ctx, app, 42)) - depl := must.Panic(app.NewDeployment(1, meta, domain.Production, "some-uid")) - uc := sut(src, be, initialData{ - deployments: []*domain.Deployment{&depl}, - targets: []*domain.Target{&target}, + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + deployment := fixture.Deployment( + fixture.WithDeploymentRequestedBy(user.ID()), + fixture.FromApp(app), + ) + providerErr := errors.New("provider_failed") + handler, ctx, dispatcher := arrange(t, source(nil), provider(providerErr), + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + r, err := handler(ctx, deploy.Command{ + AppID: string(deployment.ID().AppID()), + DeploymentNumber: int(deployment.ID().DeploymentNumber()), }) - r, err := uc(ctx, deploy.Command{ - AppID: string(depl.ID().AppID()), - DeploymentNumber: int(depl.ID().DeploymentNumber()), - }) + assert.Nil(t, err) + assert.Equal(t, bus.Unit, r) + + changed := assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.DeploymentStatusRunning, changed.State.Status()) - testutil.IsNil(t, err) - testutil.Equals(t, bus.Unit, r) - evt := testutil.EventIs[domain.DeploymentStateChanged](t, &depl, 2) - testutil.IsTrue(t, evt.State.StartedAt().HasValue()) - testutil.IsTrue(t, evt.State.FinishedAt().HasValue()) - testutil.Equals(t, providerErr.Error(), evt.State.ErrCode().MustGet()) - testutil.Equals(t, domain.DeploymentStatusFailed, evt.State.Status()) + changed = assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[1]) + assert.Equal(t, domain.DeploymentStatusFailed, changed.State.Status()) + assert.Equal(t, providerErr.Error(), changed.State.ErrCode().MustGet()) }) t.Run("should mark the deployment has succeeded if all is good", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "some-uid")) + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) target.Configured(target.CurrentVersion(), nil, nil) - - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "some-uid")) - src := source(nil) - meta := must.Panic(src.Prepare(ctx, app, 42)) - depl := must.Panic(app.NewDeployment(1, meta, domain.Production, "some-uid")) - uc := sut(src, provider(nil), initialData{ - deployments: []*domain.Deployment{&depl}, - targets: []*domain.Target{&target}, + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + deployment := fixture.Deployment( + fixture.WithDeploymentRequestedBy(user.ID()), + fixture.FromApp(app), + ) + handler, ctx, dispatcher := arrange(t, source(nil), provider(nil), + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + r, err := handler(ctx, deploy.Command{ + AppID: string(deployment.ID().AppID()), + DeploymentNumber: int(deployment.ID().DeploymentNumber()), }) - r, err := uc(ctx, deploy.Command{ - AppID: string(depl.ID().AppID()), - DeploymentNumber: int(depl.ID().DeploymentNumber()), - }) + assert.Nil(t, err) + assert.Equal(t, bus.Unit, r) + + changed := assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.DeploymentStatusRunning, changed.State.Status()) - testutil.IsNil(t, err) - testutil.Equals(t, bus.Unit, r) - evt := testutil.EventIs[domain.DeploymentStateChanged](t, &depl, 2) - testutil.IsTrue(t, evt.State.StartedAt().HasValue()) - testutil.IsTrue(t, evt.State.FinishedAt().HasValue()) - testutil.Equals(t, domain.DeploymentStatusSucceeded, evt.State.Status()) + changed = assert.Is[domain.DeploymentStateChanged](t, dispatcher.Signals()[1]) + assert.Equal(t, domain.DeploymentStatusSucceeded, changed.State.Status()) }) } 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/fail_pending_deployments/on_app_cleanup_requested.go b/internal/deployment/app/fail_pending_deployments/on_app_cleanup_requested.go index 0c6e6fdb..a3f77e51 100644 --- a/internal/deployment/app/fail_pending_deployments/on_app_cleanup_requested.go +++ b/internal/deployment/app/fail_pending_deployments/on_app_cleanup_requested.go @@ -11,7 +11,7 @@ import ( // When an app is about to be deleted, cancel all pending deployments func OnAppCleanupRequestedHandler(writer domain.DeploymentsWriter) bus.SignalHandler[domain.AppCleanupRequested] { return func(ctx context.Context, evt domain.AppCleanupRequested) error { - return writer.FailDeployments(ctx, domain.ErrAppCleanupRequested, domain.FailCriterias{ + return writer.FailDeployments(ctx, domain.ErrAppCleanupRequested, domain.FailCriteria{ Status: monad.Value(domain.DeploymentStatusPending), App: monad.Value(evt.ID), }) diff --git a/internal/deployment/app/fail_pending_deployments/on_app_env_changed.go b/internal/deployment/app/fail_pending_deployments/on_app_env_changed.go index 993a38f0..ec68f2eb 100644 --- a/internal/deployment/app/fail_pending_deployments/on_app_env_changed.go +++ b/internal/deployment/app/fail_pending_deployments/on_app_env_changed.go @@ -14,7 +14,7 @@ func OnAppEnvChangedHandler(writer domain.DeploymentsWriter) bus.SignalHandler[d return nil } - return writer.FailDeployments(ctx, domain.ErrAppTargetChanged, domain.FailCriterias{ + return writer.FailDeployments(ctx, domain.ErrAppTargetChanged, domain.FailCriteria{ Status: monad.Value(domain.DeploymentStatusPending), App: monad.Value(evt.ID), Environment: monad.Value(evt.Environment), diff --git a/internal/deployment/app/fail_pending_deployments/on_target_delete_requested.go b/internal/deployment/app/fail_pending_deployments/on_target_delete_requested.go index 5209f7d3..1e04ac5d 100644 --- a/internal/deployment/app/fail_pending_deployments/on_target_delete_requested.go +++ b/internal/deployment/app/fail_pending_deployments/on_target_delete_requested.go @@ -10,7 +10,7 @@ import ( func OnTargetDeleteRequestedHandler(writer domain.DeploymentsWriter) bus.SignalHandler[domain.TargetCleanupRequested] { return func(ctx context.Context, evt domain.TargetCleanupRequested) error { - return writer.FailDeployments(ctx, domain.ErrTargetCleanupRequested, domain.FailCriterias{ + return writer.FailDeployments(ctx, domain.ErrTargetCleanupRequested, domain.FailCriteria{ Status: monad.Value(domain.DeploymentStatusPending), Target: monad.Value(evt.ID), }) diff --git a/internal/deployment/app/get_deployment/get_deployment_test.go b/internal/deployment/app/get_deployment/get_deployment_test.go index 871ac981..4ac0ac53 100644 --- a/internal/deployment/app/get_deployment/get_deployment_test.go +++ b/internal/deployment/app/get_deployment/get_deployment_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/deployment/app/get_deployment" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/monad" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_Deployment(t *testing.T) { @@ -54,7 +54,7 @@ func Test_Deployment(t *testing.T) { d.ResolveServicesUrls() - testutil.DeepEquals(t, get_deployment.Services{ + assert.DeepEqual(t, get_deployment.Services{ { Name: "app", Image: "app-image", @@ -112,7 +112,7 @@ func Test_Deployment(t *testing.T) { d.ResolveServicesUrls() - testutil.DeepEquals(t, get_deployment.Services{ + assert.DeepEqual(t, get_deployment.Services{ { Name: "app", Image: "app-image", @@ -152,7 +152,7 @@ func Test_Deployment(t *testing.T) { d.ResolveServicesUrls() - testutil.DeepEquals(t, get_deployment.Services{ + assert.DeepEqual(t, get_deployment.Services{ { Name: "app", Image: "app-image", @@ -233,7 +233,7 @@ func Test_Deployment(t *testing.T) { d.ResolveServicesUrls() - testutil.DeepEquals(t, get_deployment.Services{ + assert.DeepEqual(t, get_deployment.Services{ { Name: "app", Image: "app-image", 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/group.go b/internal/deployment/app/group.go index 63b7ff46..76fc27c6 100644 --- a/internal/deployment/app/group.go +++ b/internal/deployment/app/group.go @@ -6,7 +6,7 @@ import ( // Group for deployment to prevent multiple deployment at the same time on the same // environment. -func DeploymentGroup(config domain.DeploymentConfig) string { +func DeploymentGroup(config domain.ConfigSnapshot) string { return "deployment.deployment.deploy." + config.ProjectName() } diff --git a/internal/deployment/app/promote/promote_test.go b/internal/deployment/app/promote/promote_test.go index b42c3131..74f976d5 100644 --- a/internal/deployment/app/promote/promote_test.go +++ b/internal/deployment/app/promote/promote_test.go @@ -4,60 +4,131 @@ import ( "context" "testing" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/promote" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" - "github.com/YuukanOO/seelf/internal/deployment/infra/source/raw" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" + shared "github.com/YuukanOO/seelf/pkg/domain" ) func Test_Promote(t *testing.T) { - ctx := auth.WithUserID(context.Background(), "some-uid") - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid")) - appsStore := memory.NewAppsStore(&app) - - sut := func(existingDeployments ...*domain.Deployment) bus.RequestHandler[int, promote.Command] { - deploymentsStore := memory.NewDeploymentsStore(existingDeployments...) - return promote.Handler(appsStore, deploymentsStore, deploymentsStore) + + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[int, promote.Command], + context.Context, + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return promote.Handler(context.AppsStore, context.DeploymentsStore, context.DeploymentsStore), context.Context, context.Dispatcher } t.Run("should fail if application does not exist", func(t *testing.T) { - uc := sut() - num, err := uc(ctx, promote.Command{ + handler, ctx, _ := arrange(t) + + num, err := handler(ctx, promote.Command{ AppID: "some-app-id", }) - testutil.ErrorIs(t, apperr.ErrNotFound, err) - testutil.Equals(t, 0, num) + assert.ErrorIs(t, apperr.ErrNotFound, err) + assert.Zero(t, num) }) t.Run("should fail if source deployment does not exist", func(t *testing.T) { - uc := sut() - num, err := uc(ctx, promote.Command{ + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + num, err := handler(ctx, promote.Command{ AppID: string(app.ID()), DeploymentNumber: 1, }) - testutil.ErrorIs(t, apperr.ErrNotFound, err) - testutil.Equals(t, 0, num) + assert.ErrorIs(t, apperr.ErrNotFound, err) + assert.Zero(t, num) + }) + + t.Run("should returns an err if trying to promote a production deployment", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + deployment := fixture.Deployment( + fixture.WithDeploymentRequestedBy(user.ID()), + fixture.FromApp(app), + ) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) + + number, err := handler(ctx, promote.Command{ + AppID: string(deployment.ID().AppID()), + DeploymentNumber: int(deployment.ID().DeploymentNumber()), + }) + + assert.ErrorIs(t, domain.ErrCouldNotPromoteProductionDeployment, err) + assert.Zero(t, number) }) t.Run("should correctly creates a new deployment based on the provided one", func(t *testing.T) { - dpl, _ := app.NewDeployment(1, raw.Data(""), domain.Staging, "some-uid") - uc := sut(&dpl) + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + deployment := fixture.Deployment( + fixture.WithDeploymentRequestedBy(user.ID()), + fixture.FromApp(app), + fixture.ForEnvironment(domain.Staging), + ) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) - number, err := uc(ctx, promote.Command{ - AppID: string(dpl.ID().AppID()), - DeploymentNumber: int(dpl.ID().DeploymentNumber()), + number, err := handler(ctx, promote.Command{ + AppID: string(deployment.ID().AppID()), + DeploymentNumber: int(deployment.ID().DeploymentNumber()), }) - testutil.IsNil(t, err) - testutil.Equals(t, 2, number) + assert.Nil(t, err) + assert.Equal(t, 2, number) + assert.HasLength(t, 1, dispatcher.Signals()) + created := assert.Is[domain.DeploymentCreated](t, dispatcher.Signals()[0]) + assert.DeepEqual(t, domain.DeploymentCreated{ + ID: domain.DeploymentIDFrom(app.ID(), 2), + Config: created.Config, + State: created.State, + Source: deployment.Source(), + Requested: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Requested.At())), + }, created) }) } 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/queue_deployment/queue_deployment_test.go b/internal/deployment/app/queue_deployment/queue_deployment_test.go index c0fdeb47..31bfced0 100644 --- a/internal/deployment/app/queue_deployment/queue_deployment_test.go +++ b/internal/deployment/app/queue_deployment/queue_deployment_test.go @@ -4,75 +4,125 @@ import ( "context" "testing" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/queue_deployment" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/internal/deployment/infra/source/raw" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" + shared "github.com/YuukanOO/seelf/pkg/domain" "github.com/YuukanOO/seelf/pkg/validate" ) func Test_QueueDeployment(t *testing.T) { - ctx := auth.WithUserID(context.Background(), "some-uid") - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid")) - appsStore := memory.NewAppsStore(&app) - - sut := func() bus.RequestHandler[int, queue_deployment.Command] { - deploymentsStore := memory.NewDeploymentsStore() - return queue_deployment.Handler(appsStore, deploymentsStore, deploymentsStore, raw.New()) + + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[int, queue_deployment.Command], + context.Context, + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return queue_deployment.Handler(context.AppsStore, context.DeploymentsStore, context.DeploymentsStore, raw.New()), context.Context, context.Dispatcher } - t.Run("should fail if payload is empty", func(t *testing.T) { - uc := sut() - num, err := uc(ctx, queue_deployment.Command{ - AppID: string(app.ID()), + t.Run("should fail if the app does not exist", func(t *testing.T) { + handler, ctx, _ := arrange(t) + + num, err := handler(ctx, queue_deployment.Command{ + AppID: "does-not-exist", Environment: "production", }) - testutil.ErrorIs(t, domain.ErrInvalidSourcePayload, err) - testutil.Equals(t, 0, num) + assert.ErrorIs(t, apperr.ErrNotFound, err) + assert.Zero(t, num) }) - t.Run("should fail if no environment has been given", func(t *testing.T) { - uc := sut() - num, err := uc(ctx, queue_deployment.Command{ - AppID: string(app.ID()), - }) + t.Run("should fail if payload is empty", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - testutil.Equals(t, 0, num) + num, err := handler(ctx, queue_deployment.Command{ + AppID: string(app.ID()), + Environment: "production", + }) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrInvalidEnvironmentName, validationErr["environment"]) + assert.ErrorIs(t, domain.ErrInvalidSourcePayload, err) + assert.Zero(t, num) }) - t.Run("should fail if the app does not exist", func(t *testing.T) { - uc := sut() - num, err := uc(ctx, queue_deployment.Command{ - AppID: "does-not-exist", - Environment: "production", + t.Run("should fail if no environment has been given", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + num, err := handler(ctx, queue_deployment.Command{ + AppID: string(app.ID()), }) - testutil.ErrorIs(t, apperr.ErrNotFound, err) - testutil.Equals(t, 0, num) + assert.Zero(t, num) + assert.ValidationError(t, validate.FieldErrors{ + "environment": domain.ErrInvalidEnvironmentName, + }, err) }) t.Run("should succeed if everything is good", func(t *testing.T) { - uc := sut() - num, err := uc(ctx, queue_deployment.Command{ + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + num, err := handler(ctx, queue_deployment.Command{ AppID: string(app.ID()), Environment: "production", Source: "some-payload", }) - testutil.IsNil(t, err) - testutil.Equals(t, 1, num) + assert.Nil(t, err) + assert.Equal(t, 1, num) + assert.HasLength(t, 1, dispatcher.Signals()) + created := assert.Is[domain.DeploymentCreated](t, dispatcher.Signals()[0]) + assert.DeepEqual(t, domain.DeploymentCreated{ + ID: domain.DeploymentIDFrom(app.ID(), 1), + Config: created.Config, + State: created.State, + Source: raw.Data("some-payload"), + Requested: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Requested.At())), + }, created) }) } diff --git a/internal/deployment/app/reconfigure_target/reconfigure_target_test.go b/internal/deployment/app/reconfigure_target/reconfigure_target_test.go index cb8e1015..0fb97d01 100644 --- a/internal/deployment/app/reconfigure_target/reconfigure_target_test.go +++ b/internal/deployment/app/reconfigure_target/reconfigure_target_test.go @@ -4,44 +4,73 @@ import ( "context" "testing" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/reconfigure_target" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" ) func Test_ReconfigureTarget(t *testing.T) { - sut := func(existingTargets ...*domain.Target) bus.RequestHandler[bus.UnitType, reconfigure_target.Command] { - store := memory.NewTargetsStore(existingTargets...) - return reconfigure_target.Handler(store, store) + + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[bus.UnitType, reconfigure_target.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return reconfigure_target.Handler(context.TargetsStore, context.TargetsStore), context.Dispatcher } - t.Run("should returns an err if the target does not exist", func(t *testing.T) { - uc := sut() + t.Run("should returns an error if the target does not exist", func(t *testing.T) { + handler, _ := arrange(t) + + _, err := handler(context.Background(), reconfigure_target.Command{}) + + assert.ErrorIs(t, apperr.ErrNotFound, err) + }) - _, err := uc(context.Background(), reconfigure_target.Command{}) + t.Run("should returns an error if the target is already being configured", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + handler, _ := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target)) + + _, err := handler(context.Background(), reconfigure_target.Command{ + ID: string(target.ID()), + }) - testutil.ErrorIs(t, apperr.ErrNotFound, err) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) }) - t.Run("should force the reconfiguration of the target", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(nil, true), "uid")) + t.Run("should returns an error if the target is being deleted", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) target.Configured(target.CurrentVersion(), nil, nil) + assert.Nil(t, target.RequestCleanup(false, user.ID())) + handler, _ := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target)) - uc := sut(&target) + _, err := handler(context.Background(), reconfigure_target.Command{ + ID: string(target.ID()), + }) + + assert.ErrorIs(t, domain.ErrTargetCleanupRequested, err) + }) + + t.Run("should reconfigure the target if everything is good", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + target.Configured(target.CurrentVersion(), nil, nil) + handler, dispatcher := arrange(t, fixture.WithUsers(&user), fixture.WithTargets(&target)) - _, err := uc(context.Background(), reconfigure_target.Command{ + _, err := handler(context.Background(), reconfigure_target.Command{ ID: string(target.ID()), }) - testutil.IsNil(t, err) - testutil.HasNEvents(t, &target, 3) - changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 2) - testutil.Equals(t, domain.TargetStatusConfiguring, changed.State.Status()) + assert.Nil(t, err) + assert.HasLength(t, 1, dispatcher.Signals()) + changed := assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.TargetStatusConfiguring, changed.State.Status()) }) } diff --git a/internal/deployment/app/redeploy/on_app_env_changed.go b/internal/deployment/app/redeploy/on_app_env_changed.go index cf878622..64ce5b53 100644 --- a/internal/deployment/app/redeploy/on_app_env_changed.go +++ b/internal/deployment/app/redeploy/on_app_env_changed.go @@ -39,7 +39,7 @@ func OnAppEnvChangedHandler( return err } - depl, err := app.Redeploy(source, number, auth.CurrentUser(ctx).MustGet()) + deployment, err := app.Redeploy(source, number, auth.CurrentUser(ctx).MustGet()) // Could not redeploy the latest deployment, maybe because of a configuration change, // just skip it (for example, trying to redeploy a git deployment but the vcs is now missing) @@ -47,6 +47,6 @@ func OnAppEnvChangedHandler( return nil } - return writer.Write(ctx, &depl) + return writer.Write(ctx, &deployment) } } diff --git a/internal/deployment/app/redeploy/redeploy_test.go b/internal/deployment/app/redeploy/redeploy_test.go index cb1c4691..417dfe67 100644 --- a/internal/deployment/app/redeploy/redeploy_test.go +++ b/internal/deployment/app/redeploy/redeploy_test.go @@ -4,61 +4,101 @@ import ( "context" "testing" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/redeploy" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" - "github.com/YuukanOO/seelf/internal/deployment/infra/source/raw" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" + shared "github.com/YuukanOO/seelf/pkg/domain" ) func Test_Redeploy(t *testing.T) { - ctx := auth.WithUserID(context.Background(), "some-uid") - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid")) - appsStore := memory.NewAppsStore(&app) - sut := func(existingDeployments ...*domain.Deployment) bus.RequestHandler[int, redeploy.Command] { - deploymentsStore := memory.NewDeploymentsStore(existingDeployments...) - return redeploy.Handler(appsStore, deploymentsStore, deploymentsStore) + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[int, redeploy.Command], + context.Context, + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return redeploy.Handler(context.AppsStore, context.DeploymentsStore, context.DeploymentsStore), context.Context, context.Dispatcher } - t.Run("should fail if application does not exist", func(t *testing.T) { - uc := sut() - num, err := uc(ctx, redeploy.Command{ + t.Run("should fail if the application does not exist", func(t *testing.T) { + handler, ctx, _ := arrange(t) + + num, err := handler(ctx, redeploy.Command{ AppID: "some-app-id", }) - testutil.ErrorIs(t, apperr.ErrNotFound, err) - testutil.Equals(t, 0, num) + assert.ErrorIs(t, apperr.ErrNotFound, err) + assert.Zero(t, num) }) t.Run("should fail if source deployment does not exist", func(t *testing.T) { - uc := sut() - num, err := uc(ctx, redeploy.Command{ + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + num, err := handler(ctx, redeploy.Command{ AppID: string(app.ID()), DeploymentNumber: 1, }) - testutil.ErrorIs(t, apperr.ErrNotFound, err) - testutil.Equals(t, 0, num) - + assert.ErrorIs(t, apperr.ErrNotFound, err) + assert.Zero(t, num) }) t.Run("should correctly creates a new deployment based on the provided one", func(t *testing.T) { - dpl, _ := app.NewDeployment(1, raw.Data(""), domain.Production, "some-uid") - uc := sut(&dpl) + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + deployment := fixture.Deployment( + fixture.WithDeploymentRequestedBy(user.ID()), + fixture.FromApp(app), + fixture.ForEnvironment(domain.Production), + ) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithDeployments(&deployment), + ) - num, err := uc(ctx, redeploy.Command{ - AppID: string(dpl.ID().AppID()), - DeploymentNumber: int(dpl.ID().DeploymentNumber()), + num, err := handler(ctx, redeploy.Command{ + AppID: string(deployment.ID().AppID()), + DeploymentNumber: int(deployment.ID().DeploymentNumber()), }) - testutil.IsNil(t, err) - testutil.Equals(t, 2, num) + assert.Nil(t, err) + assert.Equal(t, 2, num) + assert.HasLength(t, 1, dispatcher.Signals()) + created := assert.Is[domain.DeploymentCreated](t, dispatcher.Signals()[0]) + assert.DeepEqual(t, domain.DeploymentCreated{ + ID: domain.DeploymentIDFrom(app.ID(), 2), + Config: created.Config, + State: created.State, + Source: deployment.Source(), + Requested: shared.ActionFrom(user.ID(), assert.NotZero(t, created.Requested.At())), + }, created) }) } diff --git a/internal/deployment/app/request_app_cleanup/request_app_cleanup_test.go b/internal/deployment/app/request_app_cleanup/request_app_cleanup_test.go index 981fda82..a09c59b0 100644 --- a/internal/deployment/app/request_app_cleanup/request_app_cleanup_test.go +++ b/internal/deployment/app/request_app_cleanup/request_app_cleanup_test.go @@ -4,47 +4,69 @@ import ( "context" "testing" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/request_app_cleanup" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" + shared "github.com/YuukanOO/seelf/pkg/domain" ) func Test_RequestAppCleanup(t *testing.T) { - ctx := auth.WithUserID(context.Background(), "some-uid") - sut := func(existingApps ...*domain.App) bus.RequestHandler[bus.UnitType, request_app_cleanup.Command] { - store := memory.NewAppsStore(existingApps...) - return request_app_cleanup.Handler(store, store) + + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[bus.UnitType, request_app_cleanup.Command], + context.Context, + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return request_app_cleanup.Handler(context.AppsStore, context.AppsStore), context.Context, context.Dispatcher } t.Run("should fail if the application does not exist", func(t *testing.T) { - uc := sut() + handler, ctx, _ := arrange(t) - r, err := uc(ctx, request_app_cleanup.Command{ + r, err := handler(ctx, request_app_cleanup.Command{ ID: "some-id", }) - testutil.ErrorIs(t, apperr.ErrNotFound, err) - testutil.Equals(t, bus.Unit, r) + assert.ErrorIs(t, apperr.ErrNotFound, err) + assert.Equal(t, bus.Unit, r) }) t.Run("should mark an application has ready for deletion", func(t *testing.T) { - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid")) - uc := sut(&app) + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) - r, err := uc(ctx, request_app_cleanup.Command{ + r, err := handler(ctx, request_app_cleanup.Command{ ID: string(app.ID()), }) - testutil.IsNil(t, err) - testutil.Equals(t, bus.Unit, r) - - testutil.EventIs[domain.AppCleanupRequested](t, &app, 1) + assert.Nil(t, err) + assert.Equal(t, bus.Unit, r) + assert.HasLength(t, 1, dispatcher.Signals()) + requested := assert.Is[domain.AppCleanupRequested](t, dispatcher.Signals()[0]) + assert.DeepEqual(t, domain.AppCleanupRequested{ + ID: app.ID(), + ProductionConfig: requested.ProductionConfig, + StagingConfig: requested.StagingConfig, + Requested: shared.ActionFrom(user.ID(), assert.NotZero(t, requested.Requested.At())), + }, requested) }) } diff --git a/internal/deployment/app/request_target_cleanup/request_target_cleanup_test.go b/internal/deployment/app/request_target_cleanup/request_target_cleanup_test.go index 21196df6..e8db202d 100644 --- a/internal/deployment/app/request_target_cleanup/request_target_cleanup_test.go +++ b/internal/deployment/app/request_target_cleanup/request_target_cleanup_test.go @@ -4,82 +4,96 @@ import ( "context" "testing" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/request_target_cleanup" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/bus/spy" + shared "github.com/YuukanOO/seelf/pkg/domain" ) -type initialData struct { - targets []*domain.Target - apps []*domain.App -} - func Test_RequestTargetCleanup(t *testing.T) { - ctx := auth.WithUserID(context.Background(), "some-uid") - sut := func(existing initialData) bus.RequestHandler[bus.UnitType, request_target_cleanup.Command] { - targetsStore := memory.NewTargetsStore(existing.targets...) - appsStore := memory.NewAppsStore(existing.apps...) - return request_target_cleanup.Handler(targetsStore, targetsStore, appsStore) + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[bus.UnitType, request_target_cleanup.Command], + context.Context, + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return request_target_cleanup.Handler(context.TargetsStore, context.TargetsStore, context.AppsStore), context.Context, context.Dispatcher } t.Run("should returns an error if the target does not exist", func(t *testing.T) { - uc := sut(initialData{}) + handler, ctx, _ := arrange(t) - _, err := uc(ctx, request_target_cleanup.Command{ + _, err := handler(ctx, request_target_cleanup.Command{ ID: "some-id", }) - testutil.ErrorIs(t, apperr.ErrNotFound, err) + assert.ErrorIs(t, apperr.ErrNotFound, err) }) t.Run("should returns an error if the target has still apps using it", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true), - domain.NewProviderConfigRequirement(dummyProviderConfig{}, true), "uid")) + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) target.Configured(target.CurrentVersion(), nil, nil) - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig(target.ID()), true, true), "uid")) - - uc := sut(initialData{ - targets: []*domain.Target{&target}, - apps: []*domain.App{&app}, + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + _, err := handler(ctx, request_target_cleanup.Command{ + ID: string(target.ID()), }) - _, err := uc(ctx, request_target_cleanup.Command{ + assert.ErrorIs(t, domain.ErrTargetInUse, err) + }) + + t.Run("should returns an error if the target is configuring", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) + + _, err := handler(ctx, request_target_cleanup.Command{ ID: string(target.ID()), }) - testutil.ErrorIs(t, domain.ErrTargetInUse, err) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) }) t.Run("should correctly mark the target for cleanup", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true), - domain.NewProviderConfigRequirement(dummyProviderConfig{}, true), "uid")) + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) target.Configured(target.CurrentVersion(), nil, nil) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + ) - uc := sut(initialData{ - targets: []*domain.Target{&target}, - }) - - _, err := uc(ctx, request_target_cleanup.Command{ + _, err := handler(ctx, request_target_cleanup.Command{ ID: string(target.ID()), }) - testutil.IsNil(t, err) - testutil.HasNEvents(t, &target, 3) - evt := testutil.EventIs[domain.TargetCleanupRequested](t, &target, 2) - testutil.Equals(t, target.ID(), evt.ID) + assert.Nil(t, err) + assert.HasLength(t, 1, dispatcher.Signals()) + requested := assert.Is[domain.TargetCleanupRequested](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.TargetCleanupRequested{ + ID: target.ID(), + Requested: shared.ActionFrom(user.ID(), assert.NotZero(t, requested.Requested.At())), + }, requested) }) } - -type dummyProviderConfig struct { - domain.ProviderConfig -} diff --git a/internal/deployment/app/update_app/update_app_test.go b/internal/deployment/app/update_app/update_app_test.go index ab8d6c53..576ce916 100644 --- a/internal/deployment/app/update_app/update_app_test.go +++ b/internal/deployment/app/update_app/update_app_test.go @@ -4,317 +4,425 @@ import ( "context" "testing" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/update_app" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" + "github.com/YuukanOO/seelf/pkg/bus/spy" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" "github.com/YuukanOO/seelf/pkg/validate" ) func Test_UpdateApp(t *testing.T) { - production := domain.NewEnvironmentConfig("1") - production.HasEnvironmentVariables(domain.ServicesEnv{"app": {"DEBUG": "false"}}) - staging := domain.NewEnvironmentConfig("1") - staging.HasEnvironmentVariables(domain.ServicesEnv{"app": {"DEBUG": "false"}}) - ctx := auth.WithUserID(context.Background(), "some-uid") - - sut := func(existingApps ...*domain.App) bus.RequestHandler[string, update_app.Command] { - store := memory.NewAppsStore(existingApps...) - return update_app.Handler(store, store) + + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[string, update_app.Command], + context.Context, + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return update_app.Handler(context.AppsStore, context.AppsStore), context.Context, context.Dispatcher } t.Run("should require a valid application id", func(t *testing.T) { - uc := sut() - id, err := uc(ctx, update_app.Command{}) + handler, ctx, _ := arrange(t) + + id, err := handler(ctx, update_app.Command{}) - testutil.ErrorIs(t, apperr.ErrNotFound, err) - testutil.Equals(t, "", id) + assert.ErrorIs(t, apperr.ErrNotFound, err) + assert.Zero(t, id) }) t.Run("should update nothing if no fields are provided", func(t *testing.T) { - a := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), "some-uid")) - uc := sut(&a) - - id, err := uc(ctx, update_app.Command{ - ID: string(a.ID()), + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + id, err := handler(ctx, update_app.Command{ + ID: string(app.ID()), }) - testutil.IsNil(t, err) - testutil.Equals(t, string(a.ID()), id) - testutil.HasNEvents(t, &a, 1) + assert.Nil(t, err) + assert.Equal(t, string(app.ID()), id) + assert.HasLength(t, 0, dispatcher.Signals()) }) t.Run("should validate new target naming availability", func(t *testing.T) { - a1 := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("1"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("2"), true, true), "some-uid")) - a2 := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("3"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("4"), true, true), "some-uid")) - uc := sut(&a1, &a2) - - _, err := uc(ctx, update_app.Command{ - ID: string(a2.ID()), + user := authfixture.User() + targetOne := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + targetTwo := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + appOne := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithAppName("my-app"), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(targetOne.ID()), + domain.NewEnvironmentConfig(targetOne.ID()), + ), + ) + appTwo := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithAppName("my-app"), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(targetTwo.ID()), + domain.NewEnvironmentConfig(targetTwo.ID()), + ), + ) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&targetOne, &targetTwo), + fixture.WithApps(&appOne, &appTwo), + ) + + _, err := handler(ctx, update_app.Command{ + ID: string(appTwo.ID()), Production: monad.Value(update_app.EnvironmentConfig{ - Target: "1", + Target: string(targetOne.ID()), }), Staging: monad.Value(update_app.EnvironmentConfig{ - Target: "2", + Target: string(targetOne.ID()), }), }) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrAppNameAlreadyTaken, validationErr["production.target"]) - testutil.ErrorIs(t, domain.ErrAppNameAlreadyTaken, validationErr["staging.target"]) + assert.ValidationError(t, validate.FieldErrors{ + "production.target": domain.ErrAppNameAlreadyTaken, + "staging.target": domain.ErrAppNameAlreadyTaken, + }, err) }) t.Run("should remove an application env variables", func(t *testing.T) { - a := must.Panic(domain.NewApp("an-app", - domain.NewEnvironmentConfigRequirement(production, true, true), - domain.NewEnvironmentConfigRequirement(staging, true, true), - "uid", - )) - - uc := sut(&a) - - id, err := uc(ctx, update_app.Command{ - ID: string(a.ID()), + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + otherTarget := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + configWithEnvVariables := domain.NewEnvironmentConfig(target.ID()) + configWithEnvVariables.HasEnvironmentVariables(domain.ServicesEnv{ + "app": {"DEBUG": "false"}, + }) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + configWithEnvVariables, + configWithEnvVariables, + ), + ) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target, &otherTarget), + fixture.WithApps(&app), + ) + + id, err := handler(ctx, update_app.Command{ + ID: string(app.ID()), Production: monad.Value(update_app.EnvironmentConfig{ - Target: "new-production-target", + Target: string(otherTarget.ID()), }), Staging: monad.Value(update_app.EnvironmentConfig{ - Target: "new-staging-target", + Target: string(otherTarget.ID()), }), }) - testutil.IsNil(t, err) - testutil.Equals(t, string(a.ID()), id) - testutil.HasNEvents(t, &a, 3) - - evt := testutil.EventIs[domain.AppEnvChanged](t, &a, 1) + assert.Nil(t, err) + assert.Equal(t, string(app.ID()), id) + assert.HasLength(t, 2, dispatcher.Signals()) - testutil.Equals(t, domain.Production, evt.Environment) - testutil.Equals(t, "new-production-target", evt.Config.Target()) - testutil.IsFalse(t, evt.Config.Vars().HasValue()) + changed := assert.Is[domain.AppEnvChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.Production, changed.Environment) + assert.Equal(t, otherTarget.ID(), changed.Config.Target()) + assert.False(t, changed.Config.Vars().HasValue()) - evt = testutil.EventIs[domain.AppEnvChanged](t, &a, 2) - - testutil.Equals(t, domain.Staging, evt.Environment) - testutil.Equals(t, "new-staging-target", evt.Config.Target()) - testutil.IsFalse(t, evt.Config.Vars().HasValue()) + changed = assert.Is[domain.AppEnvChanged](t, dispatcher.Signals()[1]) + assert.Equal(t, domain.Staging, changed.Environment) + assert.Equal(t, otherTarget.ID(), changed.Config.Target()) + assert.False(t, changed.Config.Vars().HasValue()) }) t.Run("should update an application env variables", func(t *testing.T) { - a := must.Panic(domain.NewApp("an-app", - domain.NewEnvironmentConfigRequirement(production, true, true), - domain.NewEnvironmentConfigRequirement(staging, true, true), - "uid", - )) - - uc := sut(&a) - - id, err := uc(ctx, update_app.Command{ - ID: string(a.ID()), + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + configWithEnvVariables := domain.NewEnvironmentConfig(target.ID()) + configWithEnvVariables.HasEnvironmentVariables(domain.ServicesEnv{ + "app": {"DEBUG": "false"}, + }) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + configWithEnvVariables, + configWithEnvVariables, + ), + ) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + id, err := handler(ctx, update_app.Command{ + ID: string(app.ID()), Production: monad.Value(update_app.EnvironmentConfig{ - Target: "new-production-target", + Target: string(target.ID()), Vars: monad.Value(map[string]map[string]string{ "app": {"OTHER": "value"}, }), }), Staging: monad.Value(update_app.EnvironmentConfig{ - Target: "new-staging-target", + Target: string(target.ID()), Vars: monad.Value(map[string]map[string]string{ "app": {"SOMETHING": "else"}, }), }), }) - testutil.IsNil(t, err) - testutil.Equals(t, string(a.ID()), id) - testutil.HasNEvents(t, &a, 3) + assert.Nil(t, err) + assert.Equal(t, string(app.ID()), id) + assert.HasLength(t, 2, dispatcher.Signals()) - evt := testutil.EventIs[domain.AppEnvChanged](t, &a, 1) - - testutil.Equals(t, domain.Production, evt.Environment) - testutil.Equals(t, "new-production-target", evt.Config.Target()) - testutil.DeepEquals(t, domain.ServicesEnv{ + changed := assert.Is[domain.AppEnvChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.Production, changed.Environment) + assert.Equal(t, target.ID(), changed.Config.Target()) + assert.DeepEqual(t, domain.ServicesEnv{ "app": {"OTHER": "value"}, - }, evt.Config.Vars().MustGet()) - - evt = testutil.EventIs[domain.AppEnvChanged](t, &a, 2) + }, changed.Config.Vars().MustGet()) - testutil.Equals(t, domain.Staging, evt.Environment) - testutil.Equals(t, "new-staging-target", evt.Config.Target()) - testutil.DeepEquals(t, domain.ServicesEnv{ + changed = assert.Is[domain.AppEnvChanged](t, dispatcher.Signals()[1]) + assert.Equal(t, domain.Staging, changed.Environment) + assert.Equal(t, target.ID(), changed.Config.Target()) + assert.DeepEqual(t, domain.ServicesEnv{ "app": {"SOMETHING": "else"}, - }, evt.Config.Vars().MustGet()) + }, changed.Config.Vars().MustGet()) }) t.Run("should require valid vcs inputs", func(t *testing.T) { - uc := sut() - id, err := uc(ctx, update_app.Command{ - ID: "an-app", + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + _, err := handler(ctx, update_app.Command{ + ID: string(app.ID()), VersionControl: monad.PatchValue(update_app.VersionControl{ Url: "invalid-url", }), }) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - testutil.Equals(t, "", id) + assert.ValidationError(t, validate.FieldErrors{ + "version_control.url": domain.ErrInvalidUrl, + }, err) }) t.Run("should fail if trying to update an app being deleted", func(t *testing.T) { - a := must.Panic(domain.NewApp("an-app", - domain.NewEnvironmentConfigRequirement(production, true, true), - domain.NewEnvironmentConfigRequirement(staging, true, true), - "uid", - )) - a.RequestCleanup("uid") - - uc := sut(&a) - - _, err := uc(ctx, update_app.Command{ - ID: string(a.ID()), + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + app.RequestCleanup(user.ID()) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + _, err := handler(ctx, update_app.Command{ + ID: string(app.ID()), VersionControl: monad.PatchValue(update_app.VersionControl{ Url: "https://some.url", }), }) - testutil.ErrorIs(t, domain.ErrAppCleanupRequested, err) + assert.ErrorIs(t, domain.ErrAppCleanupRequested, err) }) t.Run("should fail if trying to add a vcs config without an url defined", func(t *testing.T) { - a := must.Panic(domain.NewApp("an-app", - domain.NewEnvironmentConfigRequirement(production, true, true), - domain.NewEnvironmentConfigRequirement(staging, true, true), - "uid", - )) - - uc := sut(&a) - - id, err := uc(ctx, update_app.Command{ - ID: string(a.ID()), + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + handler, ctx, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + _, err := handler(ctx, update_app.Command{ + ID: string(app.ID()), VersionControl: monad.PatchValue(update_app.VersionControl{}), }) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - testutil.Equals(t, "", id) + assert.ValidationError(t, validate.FieldErrors{ + "version_control.url": domain.ErrInvalidUrl, + }, err) }) t.Run("should remove the vcs config if nil given", func(t *testing.T) { - a := must.Panic(domain.NewApp("an-app", - domain.NewEnvironmentConfigRequirement(production, true, true), - domain.NewEnvironmentConfigRequirement(staging, true, true), - "uid", - )) - url := must.Panic(domain.UrlFrom("https://some.url")) - a.UseVersionControl(domain.NewVersionControl(url)) - - uc := sut(&a) - - id, err := uc(ctx, update_app.Command{ - ID: string(a.ID()), + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + assert.Nil(t, app.UseVersionControl(domain.NewVersionControl(must.Panic(domain.UrlFrom("https://some.url"))))) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + id, err := handler(ctx, update_app.Command{ + ID: string(app.ID()), VersionControl: monad.Nil[update_app.VersionControl](), }) - testutil.IsNil(t, err) - testutil.Equals(t, string(a.ID()), id) - testutil.HasNEvents(t, &a, 3) - testutil.EventIs[domain.AppVersionControlRemoved](t, &a, 2) + assert.Nil(t, err) + assert.Equal(t, string(app.ID()), id) + assert.HasLength(t, 1, dispatcher.Signals()) + removed := assert.Is[domain.AppVersionControlRemoved](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.AppVersionControlRemoved{ + ID: app.ID(), + }, removed) }) - t.Run("should update the vcs url", func(t *testing.T) { - a := must.Panic(domain.NewApp("an-app", - domain.NewEnvironmentConfigRequirement(production, true, true), - domain.NewEnvironmentConfigRequirement(staging, true, true), - "uid", - )) - url := must.Panic(domain.UrlFrom("https://some.url")) - vcs := domain.NewVersionControl(url) + t.Run("should update the vcs url and keep the token if defined", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) + vcs := domain.NewVersionControl(must.Panic(domain.UrlFrom("https://some.url"))) vcs.Authenticated("a token") - a.UseVersionControl(vcs) - - uc := sut(&a) - - id, err := uc(ctx, update_app.Command{ - ID: string(a.ID()), + assert.Nil(t, app.UseVersionControl(vcs)) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + id, err := handler(ctx, update_app.Command{ + ID: string(app.ID()), VersionControl: monad.PatchValue(update_app.VersionControl{ Url: "https://some.other.url", }), }) - testutil.IsNil(t, err) - testutil.Equals(t, string(a.ID()), id) - testutil.HasNEvents(t, &a, 3) - evt := testutil.EventIs[domain.AppVersionControlConfigured](t, &a, 2) - testutil.Equals(t, "https://some.other.url", evt.Config.Url().String()) - testutil.Equals(t, "a token", evt.Config.Token().MustGet()) + assert.Nil(t, err) + assert.Equal(t, string(app.ID()), id) + assert.HasLength(t, 1, dispatcher.Signals()) + configured := assert.Is[domain.AppVersionControlConfigured](t, dispatcher.Signals()[0]) + assert.Equal(t, app.ID(), configured.ID) + assert.Equal(t, "https://some.other.url", configured.Config.Url().String()) + assert.Equal(t, "a token", configured.Config.Token().MustGet()) }) t.Run("should remove the vcs token", func(t *testing.T) { - a := must.Panic(domain.NewApp("an-app", - domain.NewEnvironmentConfigRequirement(production, true, true), - domain.NewEnvironmentConfigRequirement(staging, true, true), - "uid", - )) + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) url := must.Panic(domain.UrlFrom("https://some.url")) vcs := domain.NewVersionControl(url) vcs.Authenticated("a token") - a.UseVersionControl(vcs) - - uc := sut(&a) - - id, err := uc(ctx, update_app.Command{ - ID: string(a.ID()), + assert.Nil(t, app.UseVersionControl(vcs)) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + id, err := handler(ctx, update_app.Command{ + ID: string(app.ID()), VersionControl: monad.PatchValue(update_app.VersionControl{ Url: "https://some.url", Token: monad.Nil[string](), }), }) - testutil.IsNil(t, err) - testutil.Equals(t, string(a.ID()), id) - testutil.HasNEvents(t, &a, 3) - evt := testutil.EventIs[domain.AppVersionControlConfigured](t, &a, 2) - testutil.Equals(t, "https://some.url", evt.Config.Url().String()) - testutil.IsFalse(t, evt.Config.Token().HasValue()) + assert.Nil(t, err) + assert.Equal(t, string(app.ID()), id) + assert.HasLength(t, 1, dispatcher.Signals()) + configured := assert.Is[domain.AppVersionControlConfigured](t, dispatcher.Signals()[0]) + assert.Equal(t, app.ID(), configured.ID) + assert.Equal(t, url, configured.Config.Url()) + assert.False(t, configured.Config.Token().HasValue()) }) t.Run("should update the vcs token", func(t *testing.T) { - a := must.Panic(domain.NewApp("an-app", - domain.NewEnvironmentConfigRequirement(production, true, true), - domain.NewEnvironmentConfigRequirement(staging, true, true), - "uid", - )) + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + app := fixture.App( + fixture.WithAppCreatedBy(user.ID()), + fixture.WithEnvironmentConfig( + domain.NewEnvironmentConfig(target.ID()), + domain.NewEnvironmentConfig(target.ID()), + ), + ) url := must.Panic(domain.UrlFrom("https://some.url")) vcs := domain.NewVersionControl(url) vcs.Authenticated("a token") - a.UseVersionControl(vcs) - - uc := sut(&a) - - id, err := uc(ctx, update_app.Command{ - ID: string(a.ID()), + assert.Nil(t, app.UseVersionControl(vcs)) + handler, ctx, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + ) + + id, err := handler(ctx, update_app.Command{ + ID: string(app.ID()), VersionControl: monad.PatchValue(update_app.VersionControl{ Url: "https://some.url", Token: monad.PatchValue("new token"), }), }) - testutil.IsNil(t, err) - testutil.Equals(t, string(a.ID()), id) - testutil.HasNEvents(t, &a, 3) - evt := testutil.EventIs[domain.AppVersionControlConfigured](t, &a, 2) - testutil.Equals(t, "https://some.url", evt.Config.Url().String()) - testutil.Equals(t, "new token", evt.Config.Token().Get("")) + assert.Nil(t, err) + assert.Equal(t, string(app.ID()), id) + assert.HasLength(t, 1, dispatcher.Signals()) + configured := assert.Is[domain.AppVersionControlConfigured](t, dispatcher.Signals()[0]) + assert.Equal(t, app.ID(), configured.ID) + assert.Equal(t, url, configured.Config.Url()) + assert.Equal(t, "new token", configured.Config.Token().Get("")) }) } diff --git a/internal/deployment/app/update_registry/update_registry_test.go b/internal/deployment/app/update_registry/update_registry_test.go index eb7812a0..3d4daafd 100644 --- a/internal/deployment/app/update_registry/update_registry_test.go +++ b/internal/deployment/app/update_registry/update_registry_test.go @@ -4,149 +4,188 @@ import ( "context" "testing" - auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/update_registry" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" + "github.com/YuukanOO/seelf/pkg/bus/spy" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" "github.com/YuukanOO/seelf/pkg/validate" ) func Test_UpdateRegistry(t *testing.T) { - ctx := auth.WithUserID(context.Background(), "some-uid") - sut := func(existing ...*domain.Registry) bus.RequestHandler[string, update_registry.Command] { - store := memory.NewRegistriesStore(existing...) - return update_registry.Handler(store, store) + + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[string, update_registry.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return update_registry.Handler(context.RegistriesStore, context.RegistriesStore), context.Dispatcher } t.Run("should require valid inputs", func(t *testing.T) { - uc := sut() + handler, _ := arrange(t) - id, err := uc(ctx, update_registry.Command{ + _, err := handler(context.Background(), update_registry.Command{ Url: monad.Value("not an url"), }) - testutil.Equals(t, "", id) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrInvalidUrl, validationErr["url"]) + assert.ValidationError(t, validate.FieldErrors{ + "url": domain.ErrInvalidUrl, + }, err) }) t.Run("should require an existing registry", func(t *testing.T) { - uc := sut() + handler, _ := arrange(t) - _, err := uc(ctx, update_registry.Command{ + _, err := handler(context.Background(), update_registry.Command{ Url: monad.Value("http://example.com"), }) - testutil.ErrorIs(t, apperr.ErrNotFound, err) + assert.ErrorIs(t, apperr.ErrNotFound, err) }) t.Run("should rename a registry", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) - uc := sut(&r) + user := authfixture.User() + registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID())) + handler, dispatcher := arrange(t, fixture.WithUsers(&user), fixture.WithRegistries(®istry)) - id, err := uc(ctx, update_registry.Command{ - ID: string(r.ID()), + id, err := handler(context.Background(), update_registry.Command{ + ID: string(registry.ID()), Name: monad.Value("new-name"), }) - testutil.NotEquals(t, "", id) - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.RegistryRenamed](t, &r, 1) - testutil.Equals(t, r.ID(), evt.ID) - testutil.Equals(t, "new-name", evt.Name) + assert.Nil(t, err) + assert.Equal(t, string(registry.ID()), id) + assert.HasLength(t, 1, dispatcher.Signals()) + renamed := assert.Is[domain.RegistryRenamed](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.RegistryRenamed{ + ID: registry.ID(), + Name: "new-name", + }, renamed) }) t.Run("should require a unique url when updating it", func(t *testing.T) { - r1 := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) - r2 := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://localhost:5000")), true), "uid")) - uc := sut(&r1, &r2) - - id, err := uc(ctx, update_registry.Command{ - ID: string(r2.ID()), + user := authfixture.User() + registry := fixture.Registry( + fixture.WithRegistryCreatedBy(user.ID()), + fixture.WithUrl(must.Panic(domain.UrlFrom("http://example.com"))), + ) + otherRegistry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID())) + handler, _ := arrange(t, + fixture.WithUsers(&user), + fixture.WithRegistries(®istry, &otherRegistry), + ) + + _, err := handler(context.Background(), update_registry.Command{ + ID: string(otherRegistry.ID()), Url: monad.Value("http://example.com"), }) - testutil.Equals(t, "", id) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, validationErr["url"]) + assert.ValidationError(t, validate.FieldErrors{ + "url": domain.ErrUrlAlreadyTaken, + }, err) }) t.Run("should update the url if its good", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) - uc := sut(&r) - - id, err := uc(ctx, update_registry.Command{ - ID: string(r.ID()), + user := authfixture.User() + registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID())) + handler, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithRegistries(®istry), + ) + + id, err := handler(context.Background(), update_registry.Command{ + ID: string(registry.ID()), Url: monad.Value("http://localhost:5000"), }) - testutil.NotEquals(t, "", id) - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.RegistryUrlChanged](t, &r, 1) - testutil.Equals(t, r.ID(), evt.ID) - testutil.Equals(t, "http://localhost:5000", evt.Url.String()) + assert.Nil(t, err) + assert.Equal(t, string(registry.ID()), id) + assert.HasLength(t, 1, dispatcher.Signals()) + changed := assert.Is[domain.RegistryUrlChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.RegistryUrlChanged{ + ID: registry.ID(), + Url: must.Panic(domain.UrlFrom("http://localhost:5000")), + }, changed) }) t.Run("should be able to add credentials", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) - uc := sut(&r) - - id, err := uc(ctx, update_registry.Command{ - ID: string(r.ID()), + user := authfixture.User() + registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID())) + handler, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithRegistries(®istry), + ) + + id, err := handler(context.Background(), update_registry.Command{ + ID: string(registry.ID()), Credentials: monad.PatchValue(update_registry.Credentials{ Username: "user", Password: monad.Value("password"), }), }) - testutil.NotEquals(t, "", id) - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.RegistryCredentialsChanged](t, &r, 1) - testutil.Equals(t, r.ID(), evt.ID) - testutil.Equals(t, "user", evt.Credentials.Username()) - testutil.Equals(t, "password", evt.Credentials.Password()) + assert.Nil(t, err) + assert.Equal(t, string(registry.ID()), id) + assert.HasLength(t, 1, dispatcher.Signals()) + changed := assert.Is[domain.RegistryCredentialsChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.RegistryCredentialsChanged{ + ID: registry.ID(), + Credentials: domain.NewCredentials("user", "password"), + }, changed) }) t.Run("should be able to update only the credentials username", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) - r.UseAuthentication(domain.NewCredentials("user", "password")) - uc := sut(&r) - - id, err := uc(ctx, update_registry.Command{ - ID: string(r.ID()), + user := authfixture.User() + registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID())) + registry.UseAuthentication(domain.NewCredentials("user", "password")) + handler, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithRegistries(®istry), + ) + + id, err := handler(context.Background(), update_registry.Command{ + ID: string(registry.ID()), Credentials: monad.PatchValue(update_registry.Credentials{ Username: "new-user", }), }) - testutil.NotEquals(t, "", id) - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.RegistryCredentialsChanged](t, &r, 2) - testutil.Equals(t, r.ID(), evt.ID) - testutil.Equals(t, "new-user", evt.Credentials.Username()) - testutil.Equals(t, "password", evt.Credentials.Password()) + assert.Nil(t, err) + assert.Equal(t, string(registry.ID()), id) + assert.HasLength(t, 1, dispatcher.Signals()) + changed := assert.Is[domain.RegistryCredentialsChanged](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.RegistryCredentialsChanged{ + ID: registry.ID(), + Credentials: domain.NewCredentials("new-user", "password"), + }, changed) }) t.Run("should be able to remove authentication", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) - r.UseAuthentication(domain.NewCredentials("user", "password")) - uc := sut(&r) - - id, err := uc(ctx, update_registry.Command{ - ID: string(r.ID()), + user := authfixture.User() + registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID())) + registry.UseAuthentication(domain.NewCredentials("user", "password")) + handler, dispatcher := arrange(t, + fixture.WithUsers(&user), + fixture.WithRegistries(®istry), + ) + + id, err := handler(context.Background(), update_registry.Command{ + ID: string(registry.ID()), Credentials: monad.Nil[update_registry.Credentials](), }) - testutil.NotEquals(t, "", id) - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.RegistryCredentialsRemoved](t, &r, 2) - testutil.Equals(t, r.ID(), evt.ID) + assert.Nil(t, err) + assert.Equal(t, string(registry.ID()), id) + assert.HasLength(t, 1, dispatcher.Signals()) + removed := assert.Is[domain.RegistryCredentialsRemoved](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.RegistryCredentialsRemoved{ + ID: registry.ID(), + }, removed) }) } 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 44a93780..28d29f29 100644 --- a/internal/deployment/app/update_target/update_target_test.go +++ b/internal/deployment/app/update_target/update_target_test.go @@ -4,97 +4,142 @@ import ( "context" "testing" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" "github.com/YuukanOO/seelf/internal/deployment/app/update_target" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/internal/deployment/infra/memory" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" + "github.com/YuukanOO/seelf/pkg/bus/spy" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" "github.com/YuukanOO/seelf/pkg/validate" ) func Test_UpdateTarget(t *testing.T) { - sut := func(existingTargets ...*domain.Target) bus.RequestHandler[string, update_target.Command] { - store := memory.NewTargetsStore(existingTargets...) - provider := &dummyProvider{} - return update_target.Handler(store, store, provider) + + arrange := func(tb testing.TB, seed ...fixture.SeedBuilder) ( + bus.RequestHandler[string, update_target.Command], + spy.Dispatcher, + ) { + context := fixture.PrepareDatabase(tb, seed...) + return update_target.Handler(context.TargetsStore, context.TargetsStore, &dummyProvider{}), context.Dispatcher } t.Run("should fail if the target does not exist", func(t *testing.T) { - uc := sut() + handler, _ := arrange(t) - _, err := uc(context.Background(), update_target.Command{}) + _, err := handler(context.Background(), update_target.Command{}) - testutil.ErrorIs(t, apperr.ErrNotFound, err) + assert.ErrorIs(t, apperr.ErrNotFound, err) }) t.Run("should fail if url or config are already taken", func(t *testing.T) { - t1 := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(dummyConfig{"1"}, true), "uid")) - t2 := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true), - domain.NewProviderConfigRequirement(dummyConfig{"2"}, true), "uid")) - uc := sut(&t1, &t2) - - _, err := uc(context.Background(), update_target.Command{ - ID: string(t1.ID()), - Provider: "2", - Url: monad.Value("http://docker.localhost"), + user := authfixture.User() + config := fixture.ProviderConfig() + targetOne := fixture.Target( + fixture.WithTargetCreatedBy(user.ID()), + fixture.WithProviderConfig(config), + ) + assert.Nil(t, targetOne.ExposeServicesAutomatically(domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true))) + targetTwo := fixture.Target( + fixture.WithTargetCreatedBy(user.ID()), + ) + 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), + ) + + _, err := handler(context.Background(), update_target.Command{ + ID: string(targetTwo.ID()), + Provider: config, + Url: monad.PatchValue("http://localhost"), }) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.ErrorIs(t, domain.ErrConfigAlreadyTaken, validationErr["dummy"]) - testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, validationErr["url"]) + assert.ValidationError(t, validate.FieldErrors{ + "url": domain.ErrUrlAlreadyTaken, + config.Kind(): domain.ErrConfigAlreadyTaken, + }, err) }) - t.Run("should update the target if everything is good", func(t *testing.T) { - target := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://localhost")), true), - domain.NewProviderConfigRequirement(dummyConfig{"1"}, true), "uid")) - uc := sut(&target) + 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) + }) - id, err := uc(context.Background(), update_target.Command{ + 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"), fixture.WithKind("test")) + + id, err := handler(context.Background(), update_target.Command{ ID: string(target.ID()), Name: monad.Value("new name"), - Provider: "1", - Url: monad.Value("http://docker.localhost"), + Provider: newConfig, + Url: monad.PatchValue("http://docker.localhost"), }) - testutil.IsNil(t, err) - testutil.Equals(t, string(target.ID()), id) - testutil.HasNEvents(t, &target, 6) - - renamed := testutil.EventIs[domain.TargetRenamed](t, &target, 1) - testutil.Equals(t, "new name", renamed.Name) - urlChanged := testutil.EventIs[domain.TargetUrlChanged](t, &target, 2) - testutil.Equals(t, "http://docker.localhost", urlChanged.Url.String()) - providerChanged := testutil.EventIs[domain.TargetProviderChanged](t, &target, 4) - testutil.Equals(t, domain.ProviderConfig(dummyConfig{"1"}), providerChanged.Provider) - testutil.EventIs[domain.TargetStateChanged](t, &target, 3) - testutil.EventIs[domain.TargetStateChanged](t, &target, 5) + assert.Nil(t, err) + assert.Equal(t, string(target.ID()), id) + assert.HasLength(t, 5, dispatcher.Signals()) + + renamed := assert.Is[domain.TargetRenamed](t, dispatcher.Signals()[0]) + assert.Equal(t, domain.TargetRenamed{ + ID: target.ID(), + Name: "new name", + }, renamed) + + urlChanged := assert.Is[domain.TargetUrlChanged](t, dispatcher.Signals()[1]) + assert.Equal(t, domain.TargetUrlChanged{ + ID: target.ID(), + Url: must.Panic(domain.UrlFrom("http://docker.localhost")), + }, urlChanged) + + assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[2]) + + providerChanged := assert.Is[domain.TargetProviderChanged](t, dispatcher.Signals()[3]) + assert.Equal(t, domain.TargetProviderChanged{ + ID: target.ID(), + Provider: newConfig, + }, providerChanged) + + assert.Is[domain.TargetStateChanged](t, dispatcher.Signals()[4]) }) } -type ( - dummyProvider struct { - domain.Provider - } - - dummyConfig struct { - data string - } -) +type dummyProvider struct { + domain.Provider +} func (*dummyProvider) Prepare(ctx context.Context, payload any, existing ...domain.ProviderConfig) (domain.ProviderConfig, error) { - return dummyConfig{payload.(string)}, nil + return payload.(domain.ProviderConfig), nil } - -func (dummyConfig) Kind() string { return "dummy" } -func (c dummyConfig) Fingerprint() string { return c.data } -func (c dummyConfig) Equals(other domain.ProviderConfig) bool { return false } -func (c dummyConfig) String() string { return c.data } diff --git a/internal/deployment/domain/app.go b/internal/deployment/domain/app.go index aebf6dc4..5413bf35 100644 --- a/internal/deployment/domain/app.go +++ b/internal/deployment/domain/app.go @@ -238,12 +238,12 @@ func (a *App) RemoveVersionControl() error { // Updates the production configuration for this application. func (a *App) HasProductionConfig(configRequirement EnvironmentConfigRequirement) error { - return a.tryUpdateEnvironmentConfig(Production, configRequirement) + return a.tryUpdateEnvironmentConfig(Production, a.production, configRequirement) } // Updates the staging configuration for this application. func (a *App) HasStagingConfig(configRequirement EnvironmentConfigRequirement) error { - return a.tryUpdateEnvironmentConfig(Staging, configRequirement) + return a.tryUpdateEnvironmentConfig(Staging, a.staging, configRequirement) } // Request cleaning for this application. This marks the application for deletion. @@ -275,28 +275,16 @@ func (a *App) Delete(cleanedUp bool) error { func (a *App) ID() AppID { return a.id } func (a *App) VersionControl() monad.Maybe[VersionControl] { return a.versionControl } -func (a *App) Production() EnvironmentConfig { return a.production } -func (a *App) Staging() EnvironmentConfig { return a.staging } func (a *App) tryUpdateEnvironmentConfig( env Environment, + existingConfig EnvironmentConfig, updatedConfigRequirement EnvironmentConfigRequirement, ) error { if a.cleanupRequested.HasValue() { return ErrAppCleanupRequested } - var existingConfig EnvironmentConfig - - switch env { - case Production: - existingConfig = a.production - case Staging: - existingConfig = a.staging - default: - return ErrInvalidEnvironmentName - } - updatedConfig, err := updatedConfigRequirement.Met() if err != nil { @@ -308,10 +296,7 @@ func (a *App) tryUpdateEnvironmentConfig( return nil } - // Same target, does not update the inner version - if updatedConfig.target == existingConfig.target { - updatedConfig.version = existingConfig.version - } + updatedConfig.consolidate(existingConfig) a.apply(AppEnvChanged{ ID: a.id, diff --git a/internal/deployment/domain/app_test.go b/internal/deployment/domain/app_test.go index 762b31a5..f43ce746 100644 --- a/internal/deployment/domain/app_test.go +++ b/internal/deployment/domain/app_test.go @@ -5,22 +5,23 @@ import ( auth "github.com/YuukanOO/seelf/internal/auth/domain" "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/internal/deployment/fixture" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" + shared "github.com/YuukanOO/seelf/pkg/domain" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_App(t *testing.T) { - var ( - appname domain.AppName = "my-app" - uid auth.UserID = "uid" - production = domain.NewEnvironmentConfig("production-target") - staging = domain.NewEnvironmentConfig("staging-target") - productionAvailable = domain.NewEnvironmentConfigRequirement(production, true, true) - stagingAvailable = domain.NewEnvironmentConfigRequirement(staging, true, true) - ) t.Run("should require a unique name across both target environments", func(t *testing.T) { + var ( + appname domain.AppName = "my-app" + uid auth.UserID = "uid" + production = domain.NewEnvironmentConfig("production-target") + staging = domain.NewEnvironmentConfig("staging-target") + ) + tests := []struct { production domain.EnvironmentConfigRequirement staging domain.EnvironmentConfigRequirement @@ -51,196 +52,227 @@ func Test_App(t *testing.T) { for _, test := range tests { _, err := domain.NewApp(appname, test.production, test.staging, uid) - testutil.ErrorIs(t, test.expected, err) + assert.ErrorIs(t, test.expected, err) } }) t.Run("should correctly creates a new app", func(t *testing.T) { - app, err := domain.NewApp(appname, productionAvailable, stagingAvailable, uid) - - testutil.IsNil(t, err) - testutil.NotEquals(t, "", app.ID()) - testutil.IsFalse(t, app.VersionControl().HasValue()) - - evt := testutil.EventIs[domain.AppCreated](t, &app, 0) - - testutil.Equals(t, app.ID(), evt.ID) - testutil.Equals(t, evt.Created.By(), uid) - testutil.IsFalse(t, evt.Created.At().IsZero()) - testutil.IsTrue(t, evt.Production.Equals(production)) - testutil.IsTrue(t, evt.Staging.Equals(staging)) - testutil.Equals(t, appname, evt.Name) + var ( + appname domain.AppName = "my-app" + uid auth.UserID = "uid" + production = domain.NewEnvironmentConfig("production-target") + staging = domain.NewEnvironmentConfig("staging-target") + ) + + app, err := domain.NewApp(appname, + domain.NewEnvironmentConfigRequirement(production, true, true), + domain.NewEnvironmentConfigRequirement(staging, true, true), + uid) + + assert.Nil(t, err) + assert.NotZero(t, app.ID()) + assert.False(t, app.VersionControl().HasValue()) + + evt := assert.EventIs[domain.AppCreated](t, &app, 0) + + assert.DeepEqual(t, domain.AppCreated{ + ID: app.ID(), + Name: appname, + Created: shared.ActionFrom(uid, assert.NotZero(t, evt.Created.At())), + Production: production, + Staging: staging, + }, evt) }) t.Run("could have a vcs config attached", func(t *testing.T) { - url := must.Panic(domain.UrlFrom("http://somewhere.com")) - vcsConfig := domain.NewVersionControl(url) + vcsConfig := domain.NewVersionControl(must.Panic(domain.UrlFrom("http://somewhere.com"))) vcsConfig.Authenticated("vcskey") - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) - app.UseVersionControl(vcsConfig) + app := fixture.App() + + err := app.UseVersionControl(vcsConfig) - testutil.Equals(t, vcsConfig, app.VersionControl().MustGet()) - testutil.HasNEvents(t, &app, 2) - evt := testutil.EventIs[domain.AppVersionControlConfigured](t, &app, 1) - testutil.Equals(t, app.ID(), evt.ID) - testutil.Equals(t, vcsConfig, evt.Config) + assert.Nil(t, err) + assert.Equal(t, vcsConfig, app.VersionControl().MustGet()) + assert.HasNEvents(t, 2, &app) + evt := assert.EventIs[domain.AppVersionControlConfigured](t, &app, 1) + + assert.Equal(t, domain.AppVersionControlConfigured{ + ID: app.ID(), + Config: vcsConfig, + }, evt) }) t.Run("could have a vcs config removed", func(t *testing.T) { - url := must.Panic(domain.UrlFrom("http://somewhere.com")) - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) - app.RemoveVersionControl() + app := fixture.App() - testutil.HasNEvents(t, &app, 1) + assert.Nil(t, app.RemoveVersionControl()) + assert.HasNEvents(t, 1, &app, "should have nothing new since it didn't have a vcs config initially") - app.UseVersionControl(domain.NewVersionControl(url)) - app.RemoveVersionControl() + assert.Nil(t, app.UseVersionControl(domain.NewVersionControl(must.Panic(domain.UrlFrom("http://somewhere.com"))))) + assert.Nil(t, app.RemoveVersionControl()) - testutil.HasNEvents(t, &app, 3) - testutil.EventIs[domain.AppVersionControlRemoved](t, &app, 2) + assert.HasNEvents(t, 3, &app, "should have 2 new events, one for the config added and one for the config removed") + assert.EventIs[domain.AppVersionControlRemoved](t, &app, 2) }) t.Run("raise a VCS configured event only if configs are different", func(t *testing.T) { - url := must.Panic(domain.UrlFrom("http://somewhere.com")) - vcsConfig := domain.NewVersionControl(url) - vcsConfig.Authenticated("vcskey") - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) - app.UseVersionControl(vcsConfig) - app.UseVersionControl(vcsConfig) - - testutil.HasNEvents(t, &app, 2) - - anotherUrl, _ := domain.UrlFrom("http://somewhere.else.com") - otherConfig := domain.NewVersionControl(anotherUrl) - app.UseVersionControl(otherConfig) - testutil.HasNEvents(t, &app, 3) - evt := testutil.EventIs[domain.AppVersionControlConfigured](t, &app, 2) - testutil.Equals(t, otherConfig, evt.Config) + vcsConfig := domain.NewVersionControl(must.Panic(domain.UrlFrom("http://somewhere.com"))) + app := fixture.App() + + assert.Nil(t, app.UseVersionControl(vcsConfig)) + assert.Nil(t, app.UseVersionControl(vcsConfig)) + + assert.HasNEvents(t, 2, &app, "should raise an event only once since the configs are equal") + + otherConfig := domain.NewVersionControl(must.Panic(domain.UrlFrom("http://somewhere.else.com"))) + assert.Nil(t, app.UseVersionControl(otherConfig)) + + assert.HasNEvents(t, 3, &app, "should raise an event since configs are different") + evt := assert.EventIs[domain.AppVersionControlConfigured](t, &app, 2) + + assert.Equal(t, domain.AppVersionControlConfigured{ + ID: app.ID(), + Config: otherConfig, + }, evt) }) t.Run("does not allow to modify the vcs config if the app is marked for deletion", func(t *testing.T) { - url := must.Panic(domain.UrlFrom("http://somewhere.com")) - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) + app := fixture.App() app.RequestCleanup("uid") - testutil.ErrorIs(t, domain.ErrAppCleanupRequested, app.UseVersionControl(domain.NewVersionControl(url))) - testutil.ErrorIs(t, domain.ErrAppCleanupRequested, app.RemoveVersionControl()) + assert.ErrorIs(t, domain.ErrAppCleanupRequested, app.UseVersionControl( + domain.NewVersionControl(must.Panic(domain.UrlFrom("http://somewhere.com"))))) + assert.ErrorIs(t, domain.ErrAppCleanupRequested, app.RemoveVersionControl()) }) t.Run("need the app naming to be available when modifying a configuration", func(t *testing.T) { - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) + app := fixture.App() - err := app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(staging, false, false)) + err := app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("another-target"), false, false)) - testutil.ErrorIs(t, apperr.ErrNotFound, err) + assert.ErrorIs(t, apperr.ErrNotFound, err) }) t.Run("should update the environment config version only if target has changed", func(t *testing.T) { - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) + config := domain.NewEnvironmentConfig("production-target") + app := fixture.App(fixture.WithEnvironmentConfig(config, config)) - newConfig := domain.NewEnvironmentConfig(production.Target()) + newConfig := domain.NewEnvironmentConfig(config.Target()) newConfig.HasEnvironmentVariables(domain.ServicesEnv{ "app": {"DEBUG": "another value"}, }) - err := app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true)) + assert.Nil(t, app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true))) + changed := assert.EventIs[domain.AppEnvChanged](t, &app, 1) - testutil.IsNil(t, err) - testutil.Equals(t, production.Version(), app.Production().Version()) + assert.Equal(t, changed.OldConfig.Version(), changed.Config.Version(), "same target should keep the same version") newConfig = domain.NewEnvironmentConfig("another-target") - err = app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true)) + assert.Nil(t, app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true))) + changed = assert.EventIs[domain.AppEnvChanged](t, &app, 2) - testutil.IsNil(t, err) - testutil.NotEquals(t, production.Version(), app.Production().Version()) - testutil.Equals(t, newConfig.Version(), app.Production().Version()) + assert.NotEqual(t, changed.OldConfig.Version(), changed.Config.Version()) + assert.Equal(t, newConfig.Version(), changed.Config.Version(), "should match the new config version") }) t.Run("raise an env changed event only if the new config is different", func(t *testing.T) { - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) + production := domain.NewEnvironmentConfig("production-target") + staging := domain.NewEnvironmentConfig("staging-target") + app := fixture.App(fixture.WithEnvironmentConfig(production, staging)) - errProd := app.HasProductionConfig(productionAvailable) - errStaging := app.HasStagingConfig(stagingAvailable) + assert.Nil(t, app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(production, true, true))) + assert.Nil(t, app.HasStagingConfig(domain.NewEnvironmentConfigRequirement(staging, true, true))) - testutil.IsNil(t, errProd) - testutil.IsNil(t, errStaging) - testutil.HasNEvents(t, &app, 1) + assert.HasNEvents(t, 1, &app, "same configs should not trigger new events") newConfig := domain.NewEnvironmentConfig("new-target") newConfig.HasEnvironmentVariables(domain.ServicesEnv{ "app": {"DEBUG": "true"}, }) - errProd = app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true)) - errStaging = app.HasStagingConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true)) - - testutil.IsNil(t, errProd) - testutil.IsNil(t, errStaging) - testutil.HasNEvents(t, &app, 3) - evt := testutil.EventIs[domain.AppEnvChanged](t, &app, 1) - - testutil.Equals(t, app.ID(), evt.ID) - testutil.Equals(t, domain.Production, evt.Environment) - testutil.DeepEquals(t, newConfig, evt.Config) + assert.Nil(t, app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true))) + assert.Nil(t, app.HasStagingConfig(domain.NewEnvironmentConfigRequirement(newConfig, true, true))) - evt = testutil.EventIs[domain.AppEnvChanged](t, &app, 2) + assert.HasNEvents(t, 3, &app, "new configs should trigger new events") + changed := assert.EventIs[domain.AppEnvChanged](t, &app, 1) - testutil.Equals(t, app.ID(), evt.ID) - testutil.Equals(t, domain.Staging, evt.Environment) - testutil.DeepEquals(t, newConfig, evt.Config) + assert.DeepEqual(t, domain.AppEnvChanged{ + ID: app.ID(), + Environment: domain.Production, + Config: newConfig, + OldConfig: production, + }, changed) + + changed = assert.EventIs[domain.AppEnvChanged](t, &app, 2) + + assert.DeepEqual(t, domain.AppEnvChanged{ + ID: app.ID(), + Environment: domain.Staging, + Config: newConfig, + OldConfig: staging, + }, changed) }) t.Run("does not allow to modify the environment config if the app is marked for deletion", func(t *testing.T) { - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) + app := fixture.App() app.RequestCleanup("uid") - testutil.ErrorIs(t, domain.ErrAppCleanupRequested, app.HasProductionConfig(productionAvailable)) - testutil.ErrorIs(t, domain.ErrAppCleanupRequested, app.HasStagingConfig(stagingAvailable)) + assert.ErrorIs(t, domain.ErrAppCleanupRequested, app.HasProductionConfig(domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("another-target"), true, true))) + assert.ErrorIs(t, domain.ErrAppCleanupRequested, app.HasStagingConfig(domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("another-target"), true, true))) }) t.Run("could be marked for deletion only if not already the case", func(t *testing.T) { - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) + production := domain.NewEnvironmentConfig("production-target") + staging := domain.NewEnvironmentConfig("staging-target") + app := fixture.App(fixture.WithEnvironmentConfig(production, staging)) app.RequestCleanup("uid") app.RequestCleanup("uid") - testutil.HasNEvents(t, &app, 2) - evt := testutil.EventIs[domain.AppCleanupRequested](t, &app, 1) - testutil.Equals(t, app.ID(), evt.ID) - testutil.Equals(t, "uid", evt.Requested.By()) + assert.HasNEvents(t, 2, &app, "should raise the event once") + evt := assert.EventIs[domain.AppCleanupRequested](t, &app, 1) + + assert.DeepEqual(t, domain.AppCleanupRequested{ + ID: app.ID(), + ProductionConfig: production, + StagingConfig: staging, + Requested: shared.ActionFrom[auth.UserID]("uid", evt.Requested.At()), + }, evt) }) t.Run("should not allow a deletion if app resources have not been cleaned up", func(t *testing.T) { - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) - + app := fixture.App() app.RequestCleanup("uid") err := app.Delete(false) - testutil.ErrorIs(t, domain.ErrAppCleanupNeeded, err) - testutil.HasNEvents(t, &app, 2) + assert.ErrorIs(t, domain.ErrAppCleanupNeeded, err) + assert.HasNEvents(t, 2, &app) }) t.Run("raise an error if delete is called for a non cleaned up app", func(t *testing.T) { - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) + app := fixture.App() err := app.Delete(false) - testutil.ErrorIs(t, domain.ErrAppCleanupNeeded, err) + assert.ErrorIs(t, domain.ErrAppCleanupNeeded, err) }) t.Run("could be deleted", func(t *testing.T) { - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) + app := fixture.App() app.RequestCleanup("uid") err := app.Delete(true) - testutil.IsNil(t, err) - testutil.HasNEvents(t, &app, 3) - evt := testutil.EventIs[domain.AppDeleted](t, &app, 2) - testutil.Equals(t, app.ID(), evt.ID) + assert.Nil(t, err) + assert.HasNEvents(t, 3, &app) + evt := assert.EventIs[domain.AppDeleted](t, &app, 2) + + assert.Equal(t, domain.AppDeleted{ + ID: app.ID(), + }, evt) }) } @@ -253,10 +285,10 @@ func Test_AppEvents(t *testing.T) { OldConfig: domain.NewEnvironmentConfig("target"), } - testutil.IsFalse(t, evt.TargetHasChanged()) + assert.False(t, evt.TargetHasChanged()) evt.OldConfig = domain.NewEnvironmentConfig("another-target") - testutil.IsTrue(t, evt.TargetHasChanged()) + assert.True(t, evt.TargetHasChanged()) }) } diff --git a/internal/deployment/domain/appname_test.go b/internal/deployment/domain/appname_test.go index 2e7fa2fe..6b5f0231 100644 --- a/internal/deployment/domain/appname_test.go +++ b/internal/deployment/domain/appname_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" ) func Test_AppNameFrom(t *testing.T) { @@ -26,15 +26,15 @@ func Test_AppNameFrom(t *testing.T) { } for _, test := range tests { - t.Run("", func(t *testing.T) { + t.Run(test.input, func(t *testing.T) { r, err := domain.AppNameFrom(test.input) if test.valid { - testutil.Equals(t, domain.AppName(test.input), r) - testutil.IsNil(t, err) + assert.Nil(t, err) + assert.Equal(t, domain.AppName(test.input), r) } else { - testutil.Equals(t, "", r) - testutil.ErrorIs(t, domain.ErrInvalidAppName, err) + assert.ErrorIs(t, domain.ErrInvalidAppName, err) + assert.Equal(t, "", r) } }) } diff --git a/internal/deployment/domain/deployment_config.go b/internal/deployment/domain/config_snapshot.go similarity index 64% rename from internal/deployment/domain/deployment_config.go rename to internal/deployment/domain/config_snapshot.go index 2dee38ec..49070776 100644 --- a/internal/deployment/domain/deployment_config.go +++ b/internal/deployment/domain/config_snapshot.go @@ -7,9 +7,9 @@ import ( ) // Holds data related to the configuration of the final application. It should -// have everything needed to resolve service and image names and is the primarly used +// have everything needed to resolve service and image names and is the primarily used // structure during the deployment by a provider. -type DeploymentConfig struct { +type ConfigSnapshot struct { appid AppID appname AppName environment Environment @@ -18,10 +18,10 @@ type DeploymentConfig struct { } // Builds a new config snapshot for the given environment. -func (a *App) ConfigSnapshotFor(env Environment) (DeploymentConfig, error) { +func (a *App) configSnapshotFor(env Environment) (ConfigSnapshot, error) { var ( conf EnvironmentConfig - snapshot DeploymentConfig + snapshot ConfigSnapshot ) switch env { @@ -42,15 +42,15 @@ func (a *App) ConfigSnapshotFor(env Environment) (DeploymentConfig, error) { return snapshot, nil } -func (c DeploymentConfig) AppID() AppID { return c.appid } -func (c DeploymentConfig) AppName() AppName { return c.appname } -func (c DeploymentConfig) Environment() Environment { return c.environment } -func (c DeploymentConfig) Target() TargetID { return c.target } -func (c DeploymentConfig) Vars() monad.Maybe[ServicesEnv] { return c.vars } // FIXME: If I want to follow my mantra, it should returns a readonly map +func (c ConfigSnapshot) AppID() AppID { return c.appid } +func (c ConfigSnapshot) AppName() AppName { return c.appname } +func (c ConfigSnapshot) Environment() Environment { return c.environment } +func (c ConfigSnapshot) Target() TargetID { return c.target } +func (c ConfigSnapshot) Vars() monad.Maybe[ServicesEnv] { return c.vars } // FIXME: If I want to follow my mantra, it should returns a readonly map // Retrieve environment variables associated with the given service name. // FIXME: If I want to follow my mantra, it should returns a readonly map -func (c DeploymentConfig) EnvironmentVariablesFor(service string) (m monad.Maybe[EnvVars]) { +func (c ConfigSnapshot) EnvironmentVariablesFor(service string) (m monad.Maybe[EnvVars]) { env, isSet := c.vars.TryGet() if !isSet { @@ -68,8 +68,14 @@ func (c DeploymentConfig) EnvironmentVariablesFor(service string) (m monad.Maybe return m } +// Retrieve the name of the project which is the combination of the appname, environment and appid +// targeted by this configuration. +func (c ConfigSnapshot) ProjectName() string { + return string(c.appname) + "-" + string(c.environment) + "-" + strings.ToLower(string(c.appid)) +} + // Returns the subdomain that will be used to expose a specific service. -func (c DeploymentConfig) SubDomain(service string, isDefault bool) string { +func (c ConfigSnapshot) subDomain(service string, isDefault bool) string { subdomain := string(c.appname) if !c.environment.IsProduction() { @@ -86,17 +92,11 @@ func (c DeploymentConfig) SubDomain(service string, isDefault bool) string { } // Builds a unique image name for the given service. -func (c DeploymentConfig) ImageName(service string) string { +func (c ConfigSnapshot) imageName(service string) string { return string(c.appname) + "-" + strings.ToLower(string(c.appid)) + "/" + service + ":" + string(c.environment) } // Builds a qualified name, truly unique, for the given service. -func (c DeploymentConfig) QualifiedName(service string) string { +func (c ConfigSnapshot) qualifiedName(service string) string { return c.ProjectName() + "-" + service } - -// Retrieve the name of the project wich is the combination of the appname, environment and appid -// targeted by this configuration. -func (c DeploymentConfig) ProjectName() string { - return string(c.appname) + "-" + string(c.environment) + "-" + strings.ToLower(string(c.appid)) -} diff --git a/internal/deployment/domain/config_snapshot_test.go b/internal/deployment/domain/config_snapshot_test.go new file mode 100644 index 00000000..9928071f --- /dev/null +++ b/internal/deployment/domain/config_snapshot_test.go @@ -0,0 +1,66 @@ +package domain_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" +) + +func Test_ConfigSnapshot(t *testing.T) { + + t.Run("could be created", func(t *testing.T) { + config := domain.NewEnvironmentConfig("production-target") + config.HasEnvironmentVariables(domain.ServicesEnv{ + "app": {"DEBUG": "false"}, + "db": {"USERNAME": "prodadmin"}, + }) + + app := fixture.App(fixture.WithAppName("my-app"), fixture.WithProductionConfig(config)) + deployment := fixture.Deployment(fixture.FromApp(app)) + conf := deployment.Config() + + assert.Equal(t, app.ID(), conf.AppID()) + assert.Equal(t, "my-app", conf.AppName()) + assert.Equal(t, domain.Production, conf.Environment()) + assert.Equal(t, config.Target(), conf.Target()) + assert.DeepEqual(t, config.Vars(), conf.Vars()) + }) + + t.Run("should provide a way to retrieve environment variables for a service name", func(t *testing.T) { + config := domain.NewEnvironmentConfig("production-target") + config.HasEnvironmentVariables(domain.ServicesEnv{ + "app": {"DEBUG": "false"}, + "db": {"USERNAME": "prodadmin"}, + }) + + app := fixture.App(fixture.WithAppName("my-app"), fixture.WithProductionConfig(config)) + deployment := fixture.Deployment(fixture.FromApp(app)) + conf := deployment.Config() + + assert.False(t, conf.EnvironmentVariablesFor("otherservice").HasValue()) + assert.True(t, conf.EnvironmentVariablesFor("app").HasValue()) + assert.DeepEqual(t, domain.EnvVars{ + "DEBUG": "false", + }, conf.EnvironmentVariablesFor("app").MustGet()) + }) + + t.Run("should return an empty monad if no environment variables are defined at all", func(t *testing.T) { + app := fixture.App() + deployment := fixture.Deployment(fixture.FromApp(app), fixture.ForEnvironment(domain.Staging)) + conf := deployment.Config() + + assert.False(t, conf.EnvironmentVariablesFor("app").HasValue()) + }) + + t.Run("should expose a unique project name", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) + deployment := fixture.Deployment(fixture.FromApp(app), fixture.ForEnvironment(domain.Staging)) + conf := deployment.Config() + + assert.Equal(t, fmt.Sprintf("my-app-staging-%s", strings.ToLower(string(app.ID()))), conf.ProjectName()) + }) +} diff --git a/internal/deployment/domain/credentials_test.go b/internal/deployment/domain/credentials_test.go index f273661a..5a55f43b 100644 --- a/internal/deployment/domain/credentials_test.go +++ b/internal/deployment/domain/credentials_test.go @@ -4,15 +4,15 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" ) func Test_Credentials(t *testing.T) { t.Run("should be instantiable", func(t *testing.T) { cred := domain.NewCredentials("user", "pass") - testutil.Equals(t, "user", cred.Username()) - testutil.Equals(t, "pass", cred.Password()) + assert.Equal(t, "user", cred.Username()) + assert.Equal(t, "pass", cred.Password()) }) t.Run("should be able to change the username", func(t *testing.T) { @@ -20,8 +20,8 @@ func Test_Credentials(t *testing.T) { cred.HasUsername("newuser") - testutil.Equals(t, "newuser", cred.Username()) - testutil.Equals(t, "pass", cred.Password()) + assert.Equal(t, "newuser", cred.Username()) + assert.Equal(t, "pass", cred.Password()) }) t.Run("should be able to change the password", func(t *testing.T) { @@ -29,7 +29,7 @@ func Test_Credentials(t *testing.T) { cred.HasPassword("newpass") - testutil.Equals(t, "user", cred.Username()) - testutil.Equals(t, "newpass", cred.Password()) + assert.Equal(t, "user", cred.Username()) + assert.Equal(t, "newpass", cred.Password()) }) } diff --git a/internal/deployment/domain/deployment.go b/internal/deployment/domain/deployment.go index d004ea73..9dbdcf28 100644 --- a/internal/deployment/domain/deployment.go +++ b/internal/deployment/domain/deployment.go @@ -17,6 +17,15 @@ var ( ErrCouldNotPromoteProductionDeployment = apperr.New("could_not_promote_production_deployment") ErrRunningOrPendingDeployments = apperr.New("running_or_pending_deployments") ErrInvalidSourceDeployment = apperr.New("invalid_source_deployment") + ErrNotInPendingState = apperr.New("not_in_pending_state") + ErrNotInRunningState = apperr.New("not_in_running_state") +) + +const ( + DeploymentStatusPending DeploymentStatus = iota + DeploymentStatusRunning + DeploymentStatusFailed + DeploymentStatusSucceeded ) type ( @@ -28,7 +37,7 @@ type ( event.Emitter id DeploymentID - config DeploymentConfig + config ConfigSnapshot state DeploymentState source SourceData requested shared.Action[domain.UserID] @@ -44,7 +53,7 @@ type ( HasDeploymentsOnAppTargetEnv(context.Context, AppID, TargetID, Environment, shared.TimeInterval) (HasRunningOrPendingDeploymentsOnAppTargetEnv, HasSuccessfulDeploymentsOnAppTargetEnv, error) } - FailCriterias struct { + FailCriteria struct { Status monad.Maybe[DeploymentStatus] Target monad.Maybe[TargetID] App monad.Maybe[AppID] @@ -52,7 +61,7 @@ type ( } DeploymentsWriter interface { - FailDeployments(context.Context, error, FailCriterias) error // Fail all deployments matching the given filters + FailDeployments(context.Context, error, FailCriteria) error // Fail all deployments matching the given filters Write(context.Context, ...*Deployment) error } @@ -60,7 +69,7 @@ type ( bus.Notification ID DeploymentID - Config DeploymentConfig + Config ConfigSnapshot State DeploymentState Source SourceData Requested shared.Action[domain.UserID] @@ -70,7 +79,7 @@ type ( bus.Notification ID DeploymentID - Config DeploymentConfig + Config ConfigSnapshot State DeploymentState } ) @@ -98,7 +107,7 @@ func (a *App) NewDeployment( return d, ErrVersionControlNotConfigured } - conf, err := a.ConfigSnapshotFor(env) + conf, err := a.configSnapshotFor(env) if err != nil { return d, err @@ -182,15 +191,13 @@ func (a *App) Promote( } func (d *Deployment) ID() DeploymentID { return d.id } -func (d *Deployment) Config() DeploymentConfig { return d.config } +func (d *Deployment) Config() ConfigSnapshot { return d.config } func (d *Deployment) Source() SourceData { return d.source } func (d *Deployment) Requested() shared.Action[domain.UserID] { return d.requested } // Mark a deployment has started. func (d *Deployment) HasStarted() error { - err := d.state.Started() - - if err != nil { + if err := d.state.started(); err != nil { return err } @@ -199,23 +206,10 @@ func (d *Deployment) HasStarted() error { return nil } -// Mark the deployment has ended with availables services or with an error if any. +// Mark the deployment has ended with available services or with an error if any. // The internal status of the deployment will be updated accordingly. func (d *Deployment) HasEnded(services Services, deploymentErr error) error { - // No services and no errors, that strange but assume a deployment without services. - if services == nil && deploymentErr == nil { - services = Services{} - } - - var err error - - if deploymentErr != nil { - err = d.state.Failed(deploymentErr) - } else { - err = d.state.Succeeded(services) - } - - if err != nil { + if err := d.state.ended(services, deploymentErr); err != nil { return err } @@ -246,3 +240,59 @@ func (d *Deployment) apply(e event.Event) { event.Store(d, e) } + +type ( + DeploymentStatus uint8 + + // Holds together information related to the current deployment state. With a value + // object, it is easier to validate consistency between all those related properties. + // The default value represents a pending state. + DeploymentState struct { + status DeploymentStatus + errcode monad.Maybe[string] + services monad.Maybe[Services] + startedAt monad.Maybe[time.Time] + finishedAt monad.Maybe[time.Time] + } +) + +func (s *DeploymentState) started() error { + if s.status != DeploymentStatusPending { + return ErrNotInPendingState + } + + s.status = DeploymentStatusRunning + s.startedAt.Set(time.Now().UTC()) + + return nil +} + +func (s *DeploymentState) ended(services Services, err error) error { + if s.status != DeploymentStatusRunning { + return ErrNotInRunningState + } + + s.finishedAt.Set(time.Now().UTC()) + + if err != nil { + s.status = DeploymentStatusFailed + s.errcode.Set(err.Error()) + return nil + } + + s.status = DeploymentStatusSucceeded + + if services == nil { + services = make(Services, 0) + } + + s.services.Set(services) + + return nil +} + +func (s DeploymentState) Status() DeploymentStatus { return s.status } +func (s DeploymentState) ErrCode() monad.Maybe[string] { return s.errcode } +func (s DeploymentState) Services() monad.Maybe[Services] { return s.services } +func (s DeploymentState) StartedAt() monad.Maybe[time.Time] { return s.startedAt } +func (s DeploymentState) FinishedAt() monad.Maybe[time.Time] { return s.finishedAt } diff --git a/internal/deployment/domain/deployment_config_test.go b/internal/deployment/domain/deployment_config_test.go deleted file mode 100644 index b2caad78..00000000 --- a/internal/deployment/domain/deployment_config_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package domain_test - -import ( - "fmt" - "strings" - "testing" - - "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" -) - -func Test_Config(t *testing.T) { - production := domain.NewEnvironmentConfig("production-target") - production.HasEnvironmentVariables(domain.ServicesEnv{ - "app": {"DEBUG": "false"}, - "db": {"USERNAME": "prodadmin"}, - }) - - staging := domain.NewEnvironmentConfig("staging-target") - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(production, true, true), - domain.NewEnvironmentConfigRequirement(staging, true, true), - "uid")) - appidLower := strings.ToLower(string(app.ID())) - - t.Run("could be created from an app", func(t *testing.T) { - conf, err := app.ConfigSnapshotFor(domain.Production) - - testutil.IsNil(t, err) - testutil.Equals(t, "my-app", conf.AppName()) - testutil.Equals(t, domain.Production, conf.Environment()) - testutil.Equals(t, production.Target(), conf.Target()) - testutil.DeepEquals(t, production.Vars(), conf.Vars()) - }) - - t.Run("should fail if env is not valid", func(t *testing.T) { - _, err := app.ConfigSnapshotFor("invalid") - - testutil.ErrorIs(t, domain.ErrInvalidEnvironmentName, err) - }) - - t.Run("should provide a way to retrieve environment variables for a service name", func(t *testing.T) { - conf, _ := app.ConfigSnapshotFor(domain.Production) - - testutil.IsFalse(t, conf.EnvironmentVariablesFor("otherservice").HasValue()) - testutil.IsTrue(t, conf.EnvironmentVariablesFor("app").HasValue()) - testutil.DeepEquals(t, domain.EnvVars{ - "DEBUG": "false", - }, conf.EnvironmentVariablesFor("app").MustGet()) - }) - - t.Run("should return an empty monad if no environment variables are defined at all", func(t *testing.T) { - conf, _ := app.ConfigSnapshotFor(domain.Staging) - - testutil.IsFalse(t, conf.EnvironmentVariablesFor("app").HasValue()) - }) - - t.Run("should generate a subdomain equals to app name if env is production", func(t *testing.T) { - conf, _ := app.ConfigSnapshotFor(domain.Production) - - testutil.Equals(t, "my-app", conf.SubDomain("app", true)) - testutil.Equals(t, "db.my-app", conf.SubDomain("db", false)) - }) - - t.Run("should generate a subdomain suffixed by the env if not production", func(t *testing.T) { - conf, _ := app.ConfigSnapshotFor(domain.Staging) - - testutil.Equals(t, "my-app-staging", conf.SubDomain("app", true)) - testutil.Equals(t, "db.my-app-staging", conf.SubDomain("db", false)) - }) - - t.Run("should expose a unique project name", func(t *testing.T) { - conf, _ := app.ConfigSnapshotFor(domain.Staging) - - testutil.Equals(t, fmt.Sprintf("my-app-staging-%s", appidLower), conf.ProjectName()) - }) - - t.Run("should expose a unique image name for a service", func(t *testing.T) { - conf, _ := app.ConfigSnapshotFor(domain.Staging) - - testutil.Equals(t, fmt.Sprintf("my-app-%s/app:staging", appidLower), conf.ImageName("app")) - }) - - t.Run("should expose a unique qualified name for a service", func(t *testing.T) { - conf, _ := app.ConfigSnapshotFor(domain.Staging) - - testutil.Equals(t, fmt.Sprintf("my-app-staging-%s-app", appidLower), conf.QualifiedName("app")) - }) -} diff --git a/internal/deployment/domain/deployment_id_test.go b/internal/deployment/domain/deployment_id_test.go index 133e972a..8f16bdc7 100644 --- a/internal/deployment/domain/deployment_id_test.go +++ b/internal/deployment/domain/deployment_id_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" ) func Test_DeploymentID(t *testing.T) { @@ -16,7 +16,7 @@ func Test_DeploymentID(t *testing.T) { id := domain.DeploymentIDFrom(app, number) - testutil.Equals(t, app, id.AppID()) - testutil.Equals(t, number, id.DeploymentNumber()) + assert.Equal(t, app, id.AppID()) + assert.Equal(t, number, id.DeploymentNumber()) }) } diff --git a/internal/deployment/domain/deployment_test.go b/internal/deployment/domain/deployment_test.go index 5508fc96..14f5801f 100644 --- a/internal/deployment/domain/deployment_test.go +++ b/internal/deployment/domain/deployment_test.go @@ -6,220 +6,265 @@ import ( auth "github.com/YuukanOO/seelf/internal/auth/domain" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" + shared "github.com/YuukanOO/seelf/pkg/domain" ) func Test_Deployment(t *testing.T) { - var ( - appname domain.AppName = "my-app" - production = domain.NewEnvironmentConfig("production-target") - staging = domain.NewEnvironmentConfig("staging-target") - productionAvailable = domain.NewEnvironmentConfigRequirement(production, true, true) - stagingAvailable = domain.NewEnvironmentConfigRequirement(staging, true, true) - uid auth.UserID = "uid" - number domain.DeploymentNumber = 1 - vcsMeta = meta{true} - nonVcsMeta = meta{false} - app = must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) - ) t.Run("should require a version control config to be defined on the app for vcs managed source", func(t *testing.T) { - _, err := app.NewDeployment(number, vcsMeta, domain.Production, uid) + app := fixture.App() + _, err := app.NewDeployment(1, fixture.SourceData(fixture.WithVersionControlNeeded()), domain.Production, "uid") - testutil.ErrorIs(t, domain.ErrVersionControlNotConfigured, err) + assert.ErrorIs(t, domain.ErrVersionControlNotConfigured, err) }) t.Run("should require an app without cleanup requested", func(t *testing.T) { - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) - app.RequestCleanup(uid) + app := fixture.App() + app.RequestCleanup("uid") - _, err := app.NewDeployment(number, nonVcsMeta, domain.Production, uid) + _, err := app.NewDeployment(1, fixture.SourceData(), domain.Production, "uid") - testutil.ErrorIs(t, domain.ErrAppCleanupRequested, err) + assert.ErrorIs(t, domain.ErrAppCleanupRequested, err) }) t.Run("should fail for an invalid environment", func(t *testing.T) { - _, err := app.NewDeployment(number, nonVcsMeta, "doesnotexist", uid) + app := fixture.App() + _, err := app.NewDeployment(1, fixture.SourceData(), "doesnotexist", "uid") - testutil.ErrorIs(t, domain.ErrInvalidEnvironmentName, err) + assert.ErrorIs(t, domain.ErrInvalidEnvironmentName, err) }) t.Run("should be created from a valid app", func(t *testing.T) { - dpl, err := app.NewDeployment(number, nonVcsMeta, domain.Production, uid) + config := domain.NewEnvironmentConfig("production-target") + app := fixture.App(fixture.WithAppName("my-app"), fixture.WithProductionConfig(config)) + sourceData := fixture.SourceData() + dpl, err := app.NewDeployment(1, sourceData, domain.Production, "uid") conf := dpl.Config() - testutil.IsNil(t, err) - testutil.Equals(t, domain.DeploymentIDFrom(app.ID(), number), dpl.ID()) - testutil.Equals(t, nonVcsMeta, dpl.Source().(meta)) - testutil.Equals(t, app.ID(), conf.AppID()) - testutil.Equals(t, "my-app", conf.AppName()) - testutil.Equals(t, domain.Production, conf.Environment()) - testutil.Equals(t, production.Target(), conf.Target()) - testutil.DeepEquals(t, production.Vars(), conf.Vars()) - - testutil.HasNEvents(t, &dpl, 1) - evt := testutil.EventIs[domain.DeploymentCreated](t, &dpl, 0) - - testutil.Equals(t, dpl.ID(), evt.ID) - testutil.Equals(t, dpl.Source(), evt.Source) - testutil.Equals(t, domain.DeploymentStatusPending, evt.State.Status()) - testutil.IsFalse(t, evt.Requested.At().IsZero()) - testutil.Equals(t, uid, evt.Requested.By()) + assert.Nil(t, err) + assert.Equal(t, domain.DeploymentIDFrom(app.ID(), 1), dpl.ID()) + assert.NotZero(t, dpl.Requested()) + assert.Equal(t, sourceData, dpl.Source()) + assert.Equal(t, app.ID(), conf.AppID()) + assert.Equal(t, "my-app", conf.AppName()) + assert.Equal(t, domain.Production, conf.Environment()) + assert.Equal(t, config.Target(), conf.Target()) + assert.DeepEqual(t, config.Vars(), conf.Vars()) + + assert.HasNEvents(t, 1, &dpl) + evt := assert.EventIs[domain.DeploymentCreated](t, &dpl, 0) + + assert.DeepEqual(t, domain.DeploymentCreated{ + ID: dpl.ID(), + Config: dpl.Config(), + State: evt.State, + Source: dpl.Source(), + Requested: shared.ActionFrom[auth.UserID]("uid", assert.NotZero(t, evt.Requested.At())), + }, evt) + + assert.Equal(t, domain.DeploymentStatusPending, evt.State.Status()) + assert.Zero(t, evt.State.ErrCode()) + assert.False(t, evt.State.Services().HasValue()) }) t.Run("could be marked has started", func(t *testing.T) { - var err error + t.Run("should fail if the deployment is already started", func(t *testing.T) { + deployment := fixture.Deployment() + assert.Nil(t, deployment.HasStarted()) - dpl, err := app.NewDeployment(number, nonVcsMeta, domain.Production, uid) + err := deployment.HasStarted() - testutil.IsNil(t, err) + assert.ErrorIs(t, domain.ErrNotInPendingState, err) + }) - err = dpl.HasStarted() + t.Run("should succeed if the deployment is not started yet", func(t *testing.T) { + deployment := fixture.Deployment() - testutil.IsNil(t, err) - testutil.HasNEvents(t, &dpl, 2) - evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 1) + err := deployment.HasStarted() - testutil.Equals(t, dpl.ID(), evt.ID) - testutil.Equals(t, domain.DeploymentStatusRunning, evt.State.Status()) + assert.Nil(t, err) + assert.HasNEvents(t, 2, &deployment) + evt := assert.EventIs[domain.DeploymentStateChanged](t, &deployment, 1) + + assert.Equal(t, deployment.ID(), evt.ID) + assert.Equal(t, domain.DeploymentStatusRunning, evt.State.Status()) + assert.NotZero(t, evt.State.StartedAt()) + assert.Zero(t, evt.State.ErrCode()) + assert.False(t, evt.State.Services().HasValue()) + assert.Zero(t, evt.State.FinishedAt()) + }) }) - t.Run("could be marked has ended with services", func(t *testing.T) { - var err error + t.Run("could be marked has ended", func(t *testing.T) { + t.Run("should fail if the deployment is not in running state", func(t *testing.T) { + deployment := fixture.Deployment() - dpl := must.Panic(app.NewDeployment(number, nonVcsMeta, domain.Production, uid)) - services := domain.Services{ - dpl.Config().NewService("aservice", "an/image"), - } + err := deployment.HasEnded(nil, nil) - dpl.HasStarted() + assert.ErrorIs(t, domain.ErrNotInRunningState, err) + }) - err = dpl.HasEnded(services, nil) + t.Run("should succeed if services is set and no error happened", func(t *testing.T) { + deployment := fixture.Deployment() + builder := deployment.Config().ServicesBuilder() + builder.AddService("service", "an/image") + services := builder.Services() + assert.Nil(t, deployment.HasStarted()) - testutil.IsNil(t, err) - testutil.HasNEvents(t, &dpl, 3) - evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 1) - testutil.Equals(t, domain.DeploymentStatusRunning, evt.State.Status()) + err := deployment.HasEnded(services, nil) - evt = testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2) + assert.Nil(t, err) + assert.HasNEvents(t, 3, &deployment, "should have events related to deployment started and ended") - testutil.Equals(t, dpl.ID(), evt.ID) - testutil.Equals(t, domain.DeploymentStatusSucceeded, evt.State.Status()) - testutil.DeepEquals(t, services, evt.State.Services().MustGet()) - }) + evt := assert.EventIs[domain.DeploymentStateChanged](t, &deployment, 2) - t.Run("should default to a deployment without services if has ended without services nor error", func(t *testing.T) { - var err error + assert.Equal(t, deployment.ID(), evt.ID) + assert.Equal(t, domain.DeploymentStatusSucceeded, evt.State.Status()) + assert.DeepEqual(t, services, evt.State.Services().MustGet()) + assert.NotZero(t, evt.State.StartedAt()) + assert.NotZero(t, evt.State.FinishedAt()) + assert.Zero(t, evt.State.ErrCode()) + }) - dpl, _ := app.NewDeployment(number, nonVcsMeta, domain.Production, uid) - dpl.HasStarted() + t.Run("should default to an empty services array if nil given and no error", func(t *testing.T) { + deployment := fixture.Deployment() + assert.Nil(t, deployment.HasStarted()) - err = dpl.HasEnded(nil, nil) + err := deployment.HasEnded(nil, nil) - testutil.IsNil(t, err) - testutil.HasNEvents(t, &dpl, 3) - evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2) + assert.Nil(t, err) + assert.HasNEvents(t, 3, &deployment, "should have events related to deployment started and ended") - testutil.Equals(t, dpl.ID(), evt.ID) - testutil.Equals(t, domain.DeploymentStatusSucceeded, evt.State.Status()) - testutil.IsTrue(t, evt.State.Services().HasValue()) - }) + evt := assert.EventIs[domain.DeploymentStateChanged](t, &deployment, 2) + + assert.Equal(t, deployment.ID(), evt.ID) + assert.Equal(t, domain.DeploymentStatusSucceeded, evt.State.Status()) + assert.DeepEqual(t, domain.Services{}, evt.State.Services().MustGet()) + assert.NotZero(t, evt.State.StartedAt()) + assert.NotZero(t, evt.State.FinishedAt()) + assert.Zero(t, evt.State.ErrCode()) + }) - t.Run("could be marked has ended with an error", func(t *testing.T) { - var ( - err error - reason = errors.New("failed reason") - ) + t.Run("should be marked has failed if an error is given", func(t *testing.T) { + reason := errors.New("failed reason") + deployment := fixture.Deployment() + assert.Nil(t, deployment.HasStarted()) - dpl, _ := app.NewDeployment(number, nonVcsMeta, domain.Production, uid) - dpl.HasStarted() + err := deployment.HasEnded(nil, reason) - err = dpl.HasEnded(nil, reason) + assert.Nil(t, err) + assert.HasNEvents(t, 3, &deployment, "should have events related to deployment started and ended") - testutil.IsNil(t, err) - testutil.HasNEvents(t, &dpl, 3) - evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2) + evt := assert.EventIs[domain.DeploymentStateChanged](t, &deployment, 2) - testutil.Equals(t, dpl.ID(), evt.ID) - testutil.Equals(t, domain.DeploymentStatusFailed, evt.State.Status()) - testutil.Equals(t, reason.Error(), evt.State.ErrCode().MustGet()) - testutil.IsFalse(t, evt.State.Services().HasValue()) + assert.Equal(t, deployment.ID(), evt.ID) + assert.Equal(t, domain.DeploymentStatusFailed, evt.State.Status()) + assert.Equal(t, reason.Error(), evt.State.ErrCode().MustGet()) + assert.NotZero(t, evt.State.FinishedAt()) + assert.False(t, evt.State.Services().HasValue()) + }) }) t.Run("could be redeployed", func(t *testing.T) { - dpl := must.Panic(app.NewDeployment(number, nonVcsMeta, domain.Production, uid)) + app := fixture.App() + sourceDeployment := fixture.Deployment(fixture.FromApp(app)) + + newDeployment, err := app.Redeploy(sourceDeployment, 2, "another-user") + + assert.Nil(t, err) + assert.Equal(t, domain.DeploymentIDFrom(app.ID(), sourceDeployment.ID().DeploymentNumber()+1), newDeployment.ID()) + assert.DeepEqual(t, sourceDeployment.Config(), newDeployment.Config()) + assert.Equal(t, sourceDeployment.Source(), newDeployment.Source()) + assert.NotZero(t, newDeployment.Requested()) + + evt := assert.EventIs[domain.DeploymentCreated](t, &newDeployment, 0) + + assert.DeepEqual(t, domain.DeploymentCreated{ + ID: newDeployment.ID(), + Config: sourceDeployment.Config(), + State: evt.State, + Source: sourceDeployment.Source(), + Requested: shared.ActionFrom[auth.UserID]("another-user", assert.NotZero(t, evt.Requested.At())), + }, evt) - redpl, err := app.Redeploy(dpl, 2, "another-user") + assert.Equal(t, domain.DeploymentStatusPending, evt.State.Status()) + assert.Zero(t, evt.State.ErrCode()) + assert.False(t, evt.State.Services().HasValue()) - testutil.IsNil(t, err) - testutil.Equals(t, dpl.Config().Environment(), redpl.Config().Environment()) - testutil.Equals(t, dpl.Source(), redpl.Source()) }) t.Run("should err if trying to redeploy a deployment on the wrong app", func(t *testing.T) { - source := must.Panic(app.NewDeployment(1, nonVcsMeta, domain.Production, uid)) - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) + source := fixture.Deployment() + anotherApp := fixture.App() - _, err := app.Redeploy(source, 2, "uid") + _, err := anotherApp.Redeploy(source, 2, "uid") - testutil.ErrorIs(t, domain.ErrInvalidSourceDeployment, err) + assert.ErrorIs(t, domain.ErrInvalidSourceDeployment, err) }) t.Run("could not promote an already in production deployment", func(t *testing.T) { - dpl := must.Panic(app.NewDeployment(number, nonVcsMeta, domain.Production, uid)) + app := fixture.App() + dpl := fixture.Deployment(fixture.FromApp(app), fixture.ForEnvironment(domain.Production)) _, err := app.Promote(dpl, 2, "another-user") - testutil.ErrorIs(t, domain.ErrCouldNotPromoteProductionDeployment, err) + assert.ErrorIs(t, domain.ErrCouldNotPromoteProductionDeployment, err) }) t.Run("should err if trying to promote a deployment on the wrong app", func(t *testing.T) { - source := must.Panic(app.NewDeployment(1, nonVcsMeta, domain.Staging, uid)) - app := must.Panic(domain.NewApp(appname, productionAvailable, stagingAvailable, uid)) + source := fixture.Deployment(fixture.ForEnvironment(domain.Staging)) + anotherApp := fixture.App() - _, err := app.Promote(source, 2, "uid") + _, err := anotherApp.Promote(source, 2, "uid") - testutil.ErrorIs(t, domain.ErrInvalidSourceDeployment, err) + assert.ErrorIs(t, domain.ErrInvalidSourceDeployment, err) }) t.Run("could promote a staging deployment", func(t *testing.T) { - dpl := must.Panic(app.NewDeployment(number, nonVcsMeta, domain.Staging, uid)) - - promoted, err := app.Promote(dpl, 2, "another-user") - - testutil.IsNil(t, err) - testutil.Equals(t, domain.Production, promoted.Config().Environment()) - testutil.Equals(t, dpl.Source(), promoted.Source()) + productionConfig := domain.NewEnvironmentConfig("production-target") + app := fixture.App(fixture.WithProductionConfig(productionConfig)) + sourceDeployment := fixture.Deployment(fixture.FromApp(app), fixture.ForEnvironment(domain.Staging)) + + promoted, err := app.Promote(sourceDeployment, 2, "another-user") + + assert.Nil(t, err) + assert.Equal(t, domain.DeploymentIDFrom(app.ID(), 2), promoted.ID()) + assert.Equal(t, sourceDeployment.Config().AppID(), promoted.Config().AppID()) + assert.Equal(t, sourceDeployment.Config().AppName(), promoted.Config().AppName()) + assert.Equal(t, productionConfig.Target(), promoted.Config().Target()) + assert.Equal(t, domain.Production, promoted.Config().Environment()) + assert.DeepEqual(t, sourceDeployment.Config().Vars(), promoted.Config().Vars()) + assert.Equal(t, sourceDeployment.Source(), promoted.Source()) + assert.NotZero(t, promoted.Requested()) + + evt := assert.EventIs[domain.DeploymentCreated](t, &promoted, 0) + + assert.DeepEqual(t, domain.DeploymentCreated{ + ID: promoted.ID(), + Config: promoted.Config(), + State: evt.State, + Source: sourceDeployment.Source(), + Requested: shared.ActionFrom[auth.UserID]("another-user", assert.NotZero(t, evt.Requested.At())), + }, evt) }) } func Test_DeploymentEvents(t *testing.T) { t.Run("DeploymentStateChanged should expose a method to check for success state", func(t *testing.T) { - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true), - "uid", - )) - dpl := must.Panic(app.NewDeployment(1, meta{}, domain.Staging, "uid")) - testutil.IsNil(t, dpl.HasStarted()) - testutil.IsNil(t, dpl.HasEnded(nil, nil)) - - evt := testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2) - testutil.IsTrue(t, evt.HasSucceeded()) - - dpl = must.Panic(app.NewDeployment(2, meta{}, domain.Staging, "uid")) - testutil.IsNil(t, dpl.HasStarted()) - testutil.IsNil(t, dpl.HasEnded(nil, errors.New("failed"))) - - evt = testutil.EventIs[domain.DeploymentStateChanged](t, &dpl, 2) - testutil.IsFalse(t, evt.HasSucceeded()) - }) -} + dpl := fixture.Deployment() + assert.Nil(t, dpl.HasStarted()) + assert.Nil(t, dpl.HasEnded(nil, nil)) -type meta struct { - isVCS bool -} + evt := assert.EventIs[domain.DeploymentStateChanged](t, &dpl, 2) + assert.True(t, evt.HasSucceeded()) + + dpl = fixture.Deployment() + assert.Nil(t, dpl.HasStarted()) + assert.Nil(t, dpl.HasEnded(nil, errors.New("failed"))) -func (meta) Kind() string { return "test" } -func (m meta) NeedVersionControl() bool { return m.isVCS } + evt = assert.EventIs[domain.DeploymentStateChanged](t, &dpl, 2) + assert.False(t, evt.HasSucceeded()) + }) +} diff --git a/internal/deployment/domain/environment.go b/internal/deployment/domain/environment.go index e3c0d723..88d1380d 100644 --- a/internal/deployment/domain/environment.go +++ b/internal/deployment/domain/environment.go @@ -73,6 +73,14 @@ func (e EnvironmentConfig) Target() TargetID { return e.target } func (e EnvironmentConfig) Version() time.Time { return e.version } func (e EnvironmentConfig) Vars() monad.Maybe[ServicesEnv] { return e.vars } +func (e *EnvironmentConfig) consolidate(other EnvironmentConfig) { + if e.target != other.target { + return + } + + e.version = other.version +} + // Builds the map of services variables from a raw value. func ServicesEnvFrom(raw map[string]map[string]string) ServicesEnv { result := make(ServicesEnv, len(raw)) diff --git a/internal/deployment/domain/environment_test.go b/internal/deployment/domain/environment_test.go index 3ac5bfd7..95a728e5 100644 --- a/internal/deployment/domain/environment_test.go +++ b/internal/deployment/domain/environment_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" ) func Test_Environment(t *testing.T) { @@ -25,11 +25,11 @@ func Test_Environment(t *testing.T) { r, err := domain.EnvironmentFrom(test.input) if test.valid { - testutil.Equals(t, domain.Environment(test.input), r) - testutil.IsNil(t, err) + assert.Equal(t, domain.Environment(test.input), r) + assert.Nil(t, err) } else { - testutil.ErrorIs(t, domain.ErrInvalidEnvironmentName, err) - testutil.Equals(t, "", r) + assert.ErrorIs(t, domain.ErrInvalidEnvironmentName, err) + assert.Equal(t, "", r) } }) } @@ -46,7 +46,7 @@ func Test_Environment(t *testing.T) { for _, test := range tests { t.Run(string(test.input), func(t *testing.T) { - testutil.Equals(t, test.production, test.input.IsProduction()) + assert.Equal(t, test.production, test.input.IsProduction()) }) } }) @@ -61,7 +61,7 @@ func Test_ServicesEnv(t *testing.T) { r := domain.ServicesEnvFrom(rawEnvs) - testutil.DeepEquals(t, domain.ServicesEnv{ + assert.DeepEqual(t, domain.ServicesEnv{ "app": {"DEBUG": "false"}, "db": {"USERNAME": "admin"}, }, r) @@ -70,7 +70,7 @@ func Test_ServicesEnv(t *testing.T) { t.Run("should returns an empty map if the raw one is nil", func(t *testing.T) { r := domain.ServicesEnvFrom(nil) - testutil.DeepEquals(t, domain.ServicesEnv{}, r) + assert.DeepEqual(t, domain.ServicesEnv{}, r) }) t.Run("should skip nil environment variables values", func(t *testing.T) { @@ -81,7 +81,7 @@ func Test_ServicesEnv(t *testing.T) { r := domain.ServicesEnvFrom(rawEnvs) - testutil.DeepEquals(t, domain.ServicesEnv{ + assert.DeepEqual(t, domain.ServicesEnv{ "app": {"DEBUG": "false"}, }, r) }) @@ -92,9 +92,9 @@ func Test_ServicesEnv(t *testing.T) { "db": {"USERNAME": "admin"}, }.Value() - testutil.IsNil(t, err) + assert.Nil(t, err) - testutil.Equals(t, `{"app":{"DEBUG":"false"},"db":{"USERNAME":"admin"}}`, str) + assert.Equal(t, `{"app":{"DEBUG":"false"},"db":{"USERNAME":"admin"}}`, str) }) t.Run("should implement the Scanner interface", func(t *testing.T) { @@ -102,8 +102,8 @@ func Test_ServicesEnv(t *testing.T) { err := r.Scan(`{"app":{"DEBUG":"false"},"db":{"USERNAME":"admin"}}`) - testutil.IsNil(t, err) - testutil.DeepEquals(t, domain.ServicesEnv{ + assert.Nil(t, err) + assert.DeepEqual(t, domain.ServicesEnv{ "app": {"DEBUG": "false"}, "db": {"USERNAME": "admin"}, }, r) @@ -116,8 +116,8 @@ func Test_EnvironmentConfig(t *testing.T) { r := domain.NewEnvironmentConfig(target) - testutil.Equals(t, target, r.Target()) - testutil.IsFalse(t, r.Vars().HasValue()) + assert.Equal(t, target, r.Target()) + assert.False(t, r.Vars().HasValue()) }) t.Run("should be able to configure environment variables", func(t *testing.T) { @@ -130,9 +130,9 @@ func Test_EnvironmentConfig(t *testing.T) { r := domain.NewEnvironmentConfig(target) r.HasEnvironmentVariables(vars) - testutil.Equals(t, target, r.Target()) - testutil.IsTrue(t, r.Vars().HasValue()) - testutil.DeepEquals(t, vars, r.Vars().MustGet()) + assert.Equal(t, target, r.Target()) + assert.True(t, r.Vars().HasValue()) + assert.DeepEqual(t, vars, r.Vars().MustGet()) }) t.Run("should be able to compare itself with another config", func(t *testing.T) { @@ -193,10 +193,10 @@ func Test_EnvironmentConfig(t *testing.T) { b := test.b() t.Run(fmt.Sprintf("%v %v", a, b), func(t *testing.T) { r := a.Equals(b) - testutil.Equals(t, test.expected, r) + assert.Equal(t, test.expected, r) r = b.Equals(a) - testutil.Equals(t, test.expected, r) + assert.Equal(t, test.expected, r) }) } }) diff --git a/internal/deployment/domain/registry_test.go b/internal/deployment/domain/registry_test.go index 81d83fba..f1d62947 100644 --- a/internal/deployment/domain/registry_test.go +++ b/internal/deployment/domain/registry_test.go @@ -3,94 +3,127 @@ package domain_test import ( "testing" + auth "github.com/YuukanOO/seelf/internal/auth/domain" "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" + shared "github.com/YuukanOO/seelf/pkg/domain" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_Registry(t *testing.T) { t.Run("should returns an error if the url is not unique", func(t *testing.T) { _, err := domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), false), "uid") - testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, err) + assert.ErrorIs(t, domain.ErrUrlAlreadyTaken, err) }) t.Run("could be created from a valid url", func(t *testing.T) { - r, err := domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid") - - testutil.IsNil(t, err) - created := testutil.EventIs[domain.RegistryCreated](t, &r, 0) - testutil.Equals(t, "http://example.com", created.Url.String()) - testutil.NotEquals(t, "", created.ID) - testutil.Equals(t, "uid", created.Created.By()) - testutil.IsFalse(t, created.Created.At().IsZero()) + var ( + url = must.Panic(domain.UrlFrom("http://example.com")) + name = "registry" + uid auth.UserID = "uid" + ) + + r, err := domain.NewRegistry(name, domain.NewRegistryUrlRequirement(url, true), uid) + + assert.Nil(t, err) + assert.NotZero(t, r.ID()) + assert.Equal(t, url, r.Url()) + assert.Equal(t, name, r.Name()) + + created := assert.EventIs[domain.RegistryCreated](t, &r, 0) + + assert.Equal(t, domain.RegistryCreated{ + ID: r.ID(), + Name: name, + Url: url, + Created: shared.ActionFrom(uid, assert.NotZero(t, created.Created.At())), + }, created) }) t.Run("could be renamed and raise the event only if different", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) + r := fixture.Registry(fixture.WithRegistryName("registry")) r.Rename("new registry") r.Rename("new registry") - testutil.HasNEvents(t, &r, 2) + assert.HasNEvents(t, 2, &r, "should raise the event once per different name") - renamed := testutil.EventIs[domain.RegistryRenamed](t, &r, 1) - testutil.Equals(t, r.ID(), renamed.ID) - testutil.Equals(t, "new registry", renamed.Name) + renamed := assert.EventIs[domain.RegistryRenamed](t, &r, 1) + + assert.Equal(t, domain.RegistryRenamed{ + ID: r.ID(), + Name: "new registry", + }, renamed) }) t.Run("should require a valid url when updating it", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) + r := fixture.Registry() err := r.HasUrl(domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://localhost:5000")), false)) - testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, err) + assert.ErrorIs(t, domain.ErrUrlAlreadyTaken, err) }) t.Run("could have its url changed and raise the event only if different", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) + r := fixture.Registry(fixture.WithUrl(must.Panic(domain.UrlFrom("http://example.com")))) + + assert.Nil(t, r.HasUrl(domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true))) - r.HasUrl(domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true)) - r.HasUrl(domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://localhost:5000")), true)) + differentUrl := must.Panic(domain.UrlFrom("http://localhost:5000")) + assert.Nil(t, r.HasUrl(domain.NewRegistryUrlRequirement(differentUrl, true))) - testutil.HasNEvents(t, &r, 2) + assert.HasNEvents(t, 2, &r, "should raise the event only if given url is different") - changed := testutil.EventIs[domain.RegistryUrlChanged](t, &r, 1) - testutil.Equals(t, r.ID(), changed.ID) - testutil.Equals(t, "http://localhost:5000", changed.Url.String()) + changed := assert.EventIs[domain.RegistryUrlChanged](t, &r, 1) + + assert.Equal(t, domain.RegistryUrlChanged{ + ID: r.ID(), + Url: differentUrl, + }, changed) }) t.Run("could have credentials attached and raise the event only if different", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) + r := fixture.Registry() + credentials := domain.NewCredentials("user", "password") - r.UseAuthentication(domain.NewCredentials("user", "password")) - r.UseAuthentication(domain.NewCredentials("user", "password")) + r.UseAuthentication(credentials) + r.UseAuthentication(credentials) + + assert.HasNEvents(t, 2, &r, "should raise the event once per different credentials") - testutil.HasNEvents(t, &r, 2) + changed := assert.EventIs[domain.RegistryCredentialsChanged](t, &r, 1) - changed := testutil.EventIs[domain.RegistryCredentialsChanged](t, &r, 1) - testutil.Equals(t, r.ID(), changed.ID) - testutil.Equals(t, "user", changed.Credentials.Username()) - testutil.Equals(t, "password", changed.Credentials.Password()) + assert.Equal(t, domain.RegistryCredentialsChanged{ + ID: r.ID(), + Credentials: credentials, + }, changed) }) t.Run("could have credentials removed and raise the event once", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) + r := fixture.Registry() r.UseAuthentication(domain.NewCredentials("user", "password")) r.RemoveAuthentication() r.RemoveAuthentication() - removed := testutil.EventIs[domain.RegistryCredentialsRemoved](t, &r, 2) - testutil.Equals(t, r.ID(), removed.ID) + removed := assert.EventIs[domain.RegistryCredentialsRemoved](t, &r, 2) + + assert.Equal(t, domain.RegistryCredentialsRemoved{ + ID: r.ID(), + }, removed) }) t.Run("could be deleted", func(t *testing.T) { - r := must.Panic(domain.NewRegistry("registry", domain.NewRegistryUrlRequirement(must.Panic(domain.UrlFrom("http://example.com")), true), "uid")) + r := fixture.Registry() r.Delete() - deleted := testutil.EventIs[domain.RegistryDeleted](t, &r, 1) - testutil.Equals(t, r.ID(), deleted.ID) + deleted := assert.EventIs[domain.RegistryDeleted](t, &r, 1) + + assert.Equal(t, domain.RegistryDeleted{ + ID: r.ID(), + }, deleted) }) } diff --git a/internal/deployment/domain/service.go b/internal/deployment/domain/service.go index 1cfa3c72..d87229b2 100644 --- a/internal/deployment/domain/service.go +++ b/internal/deployment/domain/service.go @@ -33,105 +33,131 @@ type ( port Port } - HttpEntrypointOptions struct { - // True if this entrypoint should take the default subdomain for an application. - UseDefaultSubdomain bool - // True if this entrypoint is natively managed by the target and does not require specific port exposure. - Managed bool - } - // Custom types to hold Service array which implements the Scanner and Valuer // interface to store it as a json string in the database (no need to create another table for it). Services []Service // Hold data related to services deployed upon a deployment success. Service struct { - name string - qualifiedName string - image string - entrypoints []Entrypoint + name string + image string + entrypoints []Entrypoint } -) -// Try to parse the given port from a raw string. -func ParsePort(raw string) (Port, error) { - v, err := strconv.ParseUint(raw, 10, 0) + // Main structure used to build a services array. Manipulated by actual providers. + ServicesBuilder struct { + config ConfigSnapshot + defaultSubdomainAvailable bool + services []*ServiceBuilder + } - if err != nil { - return 0, ErrInvalidPort + ServiceBuilder struct { + parent *ServicesBuilder + qualifiedName string + subdomain monad.Maybe[string] + Service } +) - return Port(v), nil +// Returns a new builder used to ease the process of building the services array. +func (c ConfigSnapshot) ServicesBuilder() ServicesBuilder { + return ServicesBuilder{ + config: c, + defaultSubdomainAvailable: true, + } } -func (p Port) String() string { return strconv.FormatUint(uint64(p), 10) } -func (p Port) Uint32() uint32 { return uint32(p) } - -func newEntrypointName(suffix string, router Router, port Port) EntrypointName { - return EntrypointName(suffix + "-" + port.String() + "-" + string(router)) -} +func (b *ServicesBuilder) AddService(name, image string) *ServiceBuilder { + // Check if the service already exists + for _, service := range b.services { + if service.Service.name == name { + return service + } + } -// Creates a new service. If the image is empty, a unique image name will be -// generated. -func (c DeploymentConfig) NewService(name, image string) (s Service) { - s.name = name - s.qualifiedName = c.QualifiedName(name) + builder := &ServiceBuilder{ + parent: b, + qualifiedName: b.config.qualifiedName(name), + Service: Service{ + name: name, + image: image, + }, + } - if image == "" { - s.image = c.ImageName(name) - } else { - s.image = image + if builder.Service.image == "" { + builder.Service.image = b.config.imageName(name) } - return s -} + b.services = append(b.services, builder) -// Adds an HTTP entrypoint to the service. -// HTTP entrypoints can be marked as automatically managed meaning they do not need a -// specific configuration and are natively handled by the target. -func (s *Service) AddHttpEntrypoint(conf DeploymentConfig, port Port, options HttpEntrypointOptions) Entrypoint { - for _, entry := range s.entrypoints { - // Already have an HTTP endpoint on this service, copy the subdomain and add it as a custom one. - if entry.router == RouterHttp { - return s.addEntrypoint(RouterHttp, !options.Managed, port, entry.subdomain.Get("")) - } - } + return builder +} - return s.addEntrypoint(RouterHttp, !options.Managed, port, conf.SubDomain(s.name, options.UseDefaultSubdomain)) +func (b *ServiceBuilder) AddHttpEntrypoint(port Port, custom bool) Entrypoint { + return b.addEntrypoint(RouterHttp, port, custom) } -// Adds a custom TCP entrypoint. -func (s *Service) AddTCPEntrypoint(port Port) Entrypoint { - return s.addEntrypoint(RouterTcp, true, port) +func (b *ServiceBuilder) AddTCPEntrypoint(port Port, custom bool) Entrypoint { + return b.addEntrypoint(RouterTcp, port, custom) } -// Adds a custom UDP entrypoint. -func (s *Service) AddUDPEntrypoint(port Port) Entrypoint { - return s.addEntrypoint(RouterUdp, true, port) +func (b *ServiceBuilder) AddUDPEntrypoint(port Port, custom bool) Entrypoint { + return b.addEntrypoint(RouterUdp, port, custom) } -func (s *Service) addEntrypoint(router Router, isCustom bool, port Port, subdomain ...string) (e Entrypoint) { - // Check if the entrypoint already exists - for _, entry := range s.entrypoints { +func (b *ServiceBuilder) addEntrypoint(router Router, port Port, custom bool) Entrypoint { + // Check if the entrypoint already exists and returns early + for _, entry := range b.Service.entrypoints { if entry.port == port && entry.router == router { return entry } } - e.name = newEntrypointName(s.qualifiedName, router, port) - e.isCustom = isCustom - e.router = router - e.port = port + entrypoint := Entrypoint{ + name: newEntrypointName(b.qualifiedName, router, port), + isCustom: custom, + router: router, + port: port, + } + + if router == RouterHttp { + if !b.subdomain.HasValue() { + b.subdomain.Set(b.parent.config.subDomain(b.Service.name, b.parent.defaultSubdomainAvailable)) + b.parent.defaultSubdomainAvailable = false + } + + entrypoint.subdomain = b.subdomain + } + + b.Service.entrypoints = append(b.Service.entrypoints, entrypoint) - if len(subdomain) > 0 { - e.subdomain.Set(subdomain[0]) + return entrypoint +} + +func (b *ServicesBuilder) Services() Services { + services := make(Services, len(b.services)) + + for i, service := range b.services { + services[i] = service.Service } - s.entrypoints = append(s.entrypoints, e) + return services +} + +// Try to parse the given port from a raw string. +func ParsePort(raw string) (Port, error) { + v, err := strconv.ParseUint(raw, 10, 0) + + if err != nil { + return 0, ErrInvalidPort + } - return e + return Port(v), nil } +func (p Port) String() string { return strconv.FormatUint(uint64(p), 10) } +func (p Port) Uint32() uint32 { return uint32(p) } + func (s Service) Name() string { return s.name } func (s Service) Image() string { return s.image } @@ -155,7 +181,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 +192,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) @@ -179,6 +205,10 @@ func isNotCustom(entrypoint Entrypoint) bool { return !entrypoint.isCustom } +func newEntrypointName(prefix string, router Router, port Port) EntrypointName { + return EntrypointName(prefix + "-" + port.String() + "-" + string(router)) +} + // Types needed to marshal an unexposed Service data. type ( marshalledEntrypoint struct { @@ -190,23 +220,21 @@ type ( } marshalledService struct { - Name string `json:"name"` - QualifiedName string `json:"qualified_name"` - Image string `json:"image"` - Entrypoints []marshalledEntrypoint `json:"entrypoints"` + Name string `json:"name"` + Image string `json:"image"` + Entrypoints []marshalledEntrypoint `json:"entrypoints"` } ) func (s Service) MarshalJSON() ([]byte, error) { - serv := marshalledService{ - Name: s.name, - QualifiedName: s.qualifiedName, - Image: s.image, - Entrypoints: make([]marshalledEntrypoint, len(s.entrypoints)), + service := marshalledService{ + Name: s.name, + Image: s.image, + Entrypoints: make([]marshalledEntrypoint, len(s.entrypoints)), } for i, entry := range s.entrypoints { - serv.Entrypoints[i] = marshalledEntrypoint{ + service.Entrypoints[i] = marshalledEntrypoint{ Name: string(entry.name), IsCustom: entry.isCustom, Router: entry.router, @@ -215,7 +243,7 @@ func (s Service) MarshalJSON() ([]byte, error) { } } - return json.Marshal(serv) + return json.Marshal(service) } func (s *Service) UnmarshalJSON(b []byte) error { @@ -227,7 +255,6 @@ func (s *Service) UnmarshalJSON(b []byte) error { s.image = m.Image s.name = m.Name - s.qualifiedName = m.QualifiedName s.entrypoints = make([]Entrypoint, len(m.Entrypoints)) for i, entry := range m.Entrypoints { diff --git a/internal/deployment/domain/service_test.go b/internal/deployment/domain/service_test.go index fc751739..8fc0bf71 100644 --- a/internal/deployment/domain/service_test.go +++ b/internal/deployment/domain/service_test.go @@ -6,145 +6,191 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" ) -func Test_Service(t *testing.T) { - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true), - "uid")) - appidLower := strings.ToLower(string(app.ID())) - config := must.Panic(app.ConfigSnapshotFor(domain.Production)) +func Test_ServicesBuilder(t *testing.T) { t.Run("could be created from a deployment configuration", func(t *testing.T) { - s := config.NewService("db", "postgres:14-alpine") + t.Run("should use the given image if any", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) + deployment := fixture.Deployment(fixture.FromApp(app)) - testutil.Equals(t, "db", s.Name()) - testutil.Equals(t, "postgres:14-alpine", s.Image()) + builder := deployment.Config().ServicesBuilder() + service := builder.AddService("db", "postgres:14-alpine") - s = config.NewService("app", "") + assert.Equal(t, "db", service.Name()) + assert.Equal(t, "postgres:14-alpine", service.Image()) + }) - testutil.Equals(t, "app", s.Name()) - testutil.Equals(t, fmt.Sprintf("my-app-%s/app:production", appidLower), s.Image()) - }) + t.Run("should generate a unique image name if not set", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) + appidLower := strings.ToLower(string(app.ID())) + deployment := fixture.Deployment(fixture.FromApp(app)) + builder := deployment.Config().ServicesBuilder() - t.Run("should populate the subdomain when adding HTTP entrypoints", func(t *testing.T) { - s := config.NewService("app", "") + service := builder.AddService("app", "") - e := s.AddHttpEntrypoint(config, 80, domain.HttpEntrypointOptions{ - Managed: true, - UseDefaultSubdomain: true, + assert.Equal(t, "app", service.Name()) + assert.Equal(t, fmt.Sprintf("my-app-%s/app:production", appidLower), service.Image()) }) - testutil.Equals(t, fmt.Sprintf("my-app-production-%s-app-80-http", appidLower), string(e.Name())) - testutil.Equals(t, domain.RouterHttp, e.Router()) - testutil.IsFalse(t, e.IsCustom()) - testutil.Equals(t, "my-app", e.Subdomain().Get("")) - testutil.Equals(t, 80, e.Port()) - - e = s.AddHttpEntrypoint(config, 8080, domain.HttpEntrypointOptions{}) - testutil.Equals(t, fmt.Sprintf("my-app-production-%s-app-8080-http", appidLower), string(e.Name())) - testutil.Equals(t, domain.RouterHttp, e.Router()) - testutil.IsTrue(t, e.IsCustom()) - testutil.Equals(t, "my-app", e.Subdomain().Get("")) - testutil.Equals(t, 8080, e.Port()) - - same := s.AddHttpEntrypoint(config, 8080, domain.HttpEntrypointOptions{}) - testutil.Equals(t, e, same) }) - t.Run("could have one or more TCP/UDP entrypoints attached", func(t *testing.T) { - s := config.NewService("app", "") - - tcp := s.AddTCPEntrypoint(8080) - testutil.Equals(t, fmt.Sprintf("my-app-production-%s-app-8080-tcp", appidLower), string(tcp.Name())) - testutil.Equals(t, domain.RouterTcp, tcp.Router()) - testutil.IsTrue(t, tcp.IsCustom()) - testutil.IsFalse(t, tcp.Subdomain().HasValue()) - testutil.Equals(t, 8080, tcp.Port()) - - udp := s.AddUDPEntrypoint(8080) - testutil.Equals(t, fmt.Sprintf("my-app-production-%s-app-8080-udp", appidLower), string(udp.Name())) - testutil.Equals(t, domain.RouterUdp, udp.Router()) - testutil.IsTrue(t, udp.IsCustom()) - testutil.IsFalse(t, udp.Subdomain().HasValue()) - testutil.Equals(t, 8080, udp.Port()) - - same := s.AddTCPEntrypoint(8080) - testutil.Equals(t, tcp, same) - - same = s.AddUDPEntrypoint(8080) - testutil.Equals(t, udp, same) + t.Run("should returns an existing service if trying to add one with the same name", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) + deployment := fixture.Deployment(fixture.FromApp(app)) + + builder := deployment.Config().ServicesBuilder() + + one := builder.AddService("app", "") + two := builder.AddService("app", "") + + assert.HasLength(t, 1, builder.Services()) + assert.Equal(t, one, two) }) -} -func Test_Services(t *testing.T) { - app := must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true), - "uid")) - appidLower := strings.ToLower(string(app.ID())) - config := must.Panic(app.ConfigSnapshotFor(domain.Production)) + t.Run("should returns the existing entrypoint if trying to add one for the same router and port", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) + deployment := fixture.Deployment(fixture.FromApp(app)) - t.Run("should be able to return all entrypoints", func(t *testing.T) { - var services domain.Services + builder := deployment.Config().ServicesBuilder() + service := builder.AddService("app", "image") + entrypointOne := service.AddHttpEntrypoint(80, true) + entrypointTwo := service.AddHttpEntrypoint(80, false) - s := config.NewService("app", "") - http := s.AddHttpEntrypoint(config, 80, domain.HttpEntrypointOptions{ - Managed: true, - }) - udp := s.AddUDPEntrypoint(8080) + assert.HasLength(t, 1, builder.Services()) + assert.Equal(t, 1, len(builder.Services().Entrypoints())) + assert.True(t, builder.Services().Entrypoints()[0].IsCustom()) + assert.Equal(t, entrypointOne, entrypointTwo) + }) - services = append(services, s) + t.Run("could have http entrypoints added", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) + appidLower := strings.ToLower(string(app.ID())) + deployment := fixture.Deployment(fixture.FromApp(app)) - s = config.NewService("db", "postgres:14-alpine") - tcp := s.AddTCPEntrypoint(5432) + builder := deployment.Config().ServicesBuilder() + service := builder.AddService("app", "") + service.AddHttpEntrypoint(80, true) + service.AddHttpEntrypoint(8080, false) + service = builder.AddService("other", "") + service.AddHttpEntrypoint(3000, false) - services = append(services, s) + services := builder.Services() + assert.HasLength(t, 2, services) + assert.Equal(t, "app", services[0].Name()) + assert.Equal(t, fmt.Sprintf("my-app-%s/app:production", appidLower), services[0].Image()) - s = config.NewService("cache", "redis:6-alpine") - services = append(services, s) + assert.Equal(t, "other", services[1].Name()) + assert.Equal(t, fmt.Sprintf("my-app-%s/other:production", appidLower), services[1].Image()) entrypoints := services.Entrypoints() + assert.HasLength(t, 3, entrypoints) + + assert.Equal(t, fmt.Sprintf("my-app-production-%s-app-80-http", appidLower), string(entrypoints[0].Name())) + assert.Equal(t, domain.RouterHttp, entrypoints[0].Router()) + assert.True(t, entrypoints[0].IsCustom()) + assert.Equal(t, 80, entrypoints[0].Port()) + assert.Equal(t, "my-app", entrypoints[0].Subdomain().Get("")) + + assert.Equal(t, fmt.Sprintf("my-app-production-%s-app-8080-http", appidLower), string(entrypoints[1].Name())) + assert.Equal(t, domain.RouterHttp, entrypoints[1].Router()) + assert.False(t, entrypoints[1].IsCustom()) + assert.Equal(t, 8080, entrypoints[1].Port()) + assert.Equal(t, "my-app", entrypoints[1].Subdomain().Get("")) + + assert.Equal(t, fmt.Sprintf("my-app-production-%s-other-3000-http", appidLower), string(entrypoints[2].Name())) + assert.Equal(t, domain.RouterHttp, entrypoints[2].Router()) + assert.False(t, entrypoints[2].IsCustom()) + assert.Equal(t, 3000, entrypoints[2].Port()) + assert.Equal(t, "other.my-app", entrypoints[2].Subdomain().Get("")) + }) - testutil.HasLength(t, entrypoints, 3) - testutil.Equals(t, http, entrypoints[0]) - testutil.Equals(t, udp, entrypoints[1]) - testutil.Equals(t, tcp, entrypoints[2]) + t.Run("could have one or more TCP/UDP entrypoints attached", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) + appidLower := strings.ToLower(string(app.ID())) + deployment := fixture.Deployment(fixture.FromApp(app)) + + builder := deployment.Config().ServicesBuilder() + + service := builder.AddService("app", "") + + tcp := service.AddTCPEntrypoint(8080, true) + assert.Equal(t, fmt.Sprintf("my-app-production-%s-app-8080-tcp", appidLower), string(tcp.Name())) + assert.Equal(t, domain.RouterTcp, tcp.Router()) + assert.True(t, tcp.IsCustom()) + assert.False(t, tcp.Subdomain().HasValue()) + assert.Equal(t, 8080, tcp.Port()) + + udp := service.AddUDPEntrypoint(8080, true) + assert.Equal(t, fmt.Sprintf("my-app-production-%s-app-8080-udp", appidLower), string(udp.Name())) + assert.Equal(t, domain.RouterUdp, udp.Router()) + assert.True(t, udp.IsCustom()) + assert.False(t, udp.Subdomain().HasValue()) + assert.Equal(t, 8080, udp.Port()) + }) +} - entrypoints = services.CustomEntrypoints() +func Test_Services(t *testing.T) { - testutil.HasLength(t, entrypoints, 2) - testutil.Equals(t, udp, entrypoints[0]) - testutil.Equals(t, tcp, entrypoints[1]) - }) + t.Run("should be able to return all entrypoints", func(t *testing.T) { + deployment := fixture.Deployment() + builder := deployment.Config().ServicesBuilder() - t.Run("should implement the valuer interface", func(t *testing.T) { - var services domain.Services + service := builder.AddService("app", "") + http := service.AddHttpEntrypoint(80, false) + udp := service.AddUDPEntrypoint(8080, true) + service = builder.AddService("db", "postgres:14-alpine") + tcp := service.AddTCPEntrypoint(5432, true) + builder.AddService("cache", "redis:6-alpine") + services := builder.Services() - s := config.NewService("app", "") - s.AddHttpEntrypoint(config, 80, domain.HttpEntrypointOptions{ - UseDefaultSubdomain: true, - Managed: true, - }) - s.AddTCPEntrypoint(8080) + entrypoints := services.Entrypoints() - services = append(services, s) + assert.HasLength(t, 3, entrypoints) + assert.Equal(t, http, entrypoints[0]) + assert.Equal(t, udp, entrypoints[1]) + assert.Equal(t, tcp, entrypoints[2]) + }) - s = config.NewService("db", "postgres:14-alpine") - s.AddTCPEntrypoint(5432) + t.Run("should be able to return all custom entrypoints", func(t *testing.T) { + deployment := fixture.Deployment() + builder := deployment.Config().ServicesBuilder() - services = append(services, s) + service := builder.AddService("app", "") + service.AddHttpEntrypoint(80, false) + udp := service.AddUDPEntrypoint(8080, true) + service = builder.AddService("db", "postgres:14-alpine") + tcp := service.AddTCPEntrypoint(5432, true) + builder.AddService("cache", "redis:6-alpine") + services := builder.Services() - s = config.NewService("cache", "redis:6-alpine") - services = append(services, s) + entrypoints := services.CustomEntrypoints() - value, err := services.Value() + assert.HasLength(t, 2, entrypoints) + assert.Equal(t, udp, entrypoints[0]) + assert.Equal(t, tcp, entrypoints[1]) + }) - testutil.IsNil(t, err) - testutil.Equals(t, fmt.Sprintf(`[{"name":"app","qualified_name":"my-app-production-%s-app","image":"my-app-%s/app:production","entrypoints":[{"name":"my-app-production-%s-app-80-http","is_custom":false,"router":"http","subdomain":"my-app","port":80},{"name":"my-app-production-%s-app-8080-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":8080}]},{"name":"db","qualified_name":"my-app-production-%s-db","image":"postgres:14-alpine","entrypoints":[{"name":"my-app-production-%s-db-5432-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":5432}]},{"name":"cache","qualified_name":"my-app-production-%s-cache","image":"redis:6-alpine","entrypoints":[]}]`, - appidLower, appidLower, appidLower, appidLower, appidLower, appidLower, appidLower), value.(string)) + t.Run("should implement the valuer interface", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("my-app")) + deployment := fixture.Deployment(fixture.FromApp(app)) + appidLower := strings.ToLower(string(deployment.ID().AppID())) + builder := deployment.Config().ServicesBuilder() + + service := builder.AddService("app", "") + service.AddHttpEntrypoint(80, false) + service.AddTCPEntrypoint(8080, true) + service = builder.AddService("db", "postgres:14-alpine") + service.AddTCPEntrypoint(5432, true) + builder.AddService("cache", "redis:6-alpine") + + value, err := builder.Services().Value() + + assert.Nil(t, err) + assert.Equal(t, fmt.Sprintf(`[{"name":"app","image":"my-app-%s/app:production","entrypoints":[{"name":"my-app-production-%s-app-80-http","is_custom":false,"router":"http","subdomain":"my-app","port":80},{"name":"my-app-production-%s-app-8080-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":8080}]},{"name":"db","image":"postgres:14-alpine","entrypoints":[{"name":"my-app-production-%s-db-5432-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":5432}]},{"name":"cache","image":"redis:6-alpine","entrypoints":[]}]`, + appidLower, appidLower, appidLower, appidLower), value.(string)) }) t.Run("should implement the scanner interface", func(t *testing.T) { @@ -194,13 +240,13 @@ func Test_Services(t *testing.T) { } ]`) - testutil.IsNil(t, err) - testutil.HasLength(t, services, 3) + assert.Nil(t, err) + assert.HasLength(t, 3, services) v, err := services.Value() - testutil.IsNil(t, err) - testutil.Equals(t, `[{"name":"app","qualified_name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app","image":"my-app-2fa8domd2sh7ehyqlxf7jvj57xs/app:production","entrypoints":[{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-80-http","is_custom":false,"router":"http","subdomain":"my-app","port":80},{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":8080}]},{"name":"db","qualified_name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-db","image":"postgres:14-alpine","entrypoints":[{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-db-5432-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":5432}]},{"name":"cache","qualified_name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-cache","image":"redis:6-alpine","entrypoints":[]}]`, v.(string)) + assert.Nil(t, err) + assert.Equal(t, `[{"name":"app","image":"my-app-2fa8domd2sh7ehyqlxf7jvj57xs/app:production","entrypoints":[{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-80-http","is_custom":false,"router":"http","subdomain":"my-app","port":80},{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":8080}]},{"name":"db","image":"postgres:14-alpine","entrypoints":[{"name":"my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-db-5432-tcp","is_custom":true,"router":"tcp","subdomain":null,"port":5432}]},{"name":"cache","image":"redis:6-alpine","entrypoints":[]}]`, v.(string)) }) } @@ -208,28 +254,28 @@ func Test_Port(t *testing.T) { t.Run("should be able to parse a port from a raw string value", func(t *testing.T) { _, err := domain.ParsePort("failed") - testutil.ErrorIs(t, domain.ErrInvalidPort, err) + assert.ErrorIs(t, domain.ErrInvalidPort, err) p, err := domain.ParsePort("8080") - testutil.IsNil(t, err) - testutil.Equals(t, 8080, p) + assert.Nil(t, err) + assert.Equal(t, 8080, p) }) t.Run("should convert the port to a string", func(t *testing.T) { p := domain.Port(8080) - testutil.Equals(t, "8080", p.String()) + assert.Equal(t, "8080", p.String()) }) t.Run("should convert the port to a uint32", func(t *testing.T) { p := domain.Port(8080) - testutil.Equals(t, 8080, p.Uint32()) + assert.Equal(t, 8080, p.Uint32()) }) } func Test_EntrypointName(t *testing.T) { t.Run("should provide a protocol", func(t *testing.T) { - testutil.Equals(t, "tcp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-http").Protocol()) - testutil.Equals(t, "tcp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-tcp").Protocol()) - testutil.Equals(t, "udp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-udp").Protocol()) + assert.Equal(t, "tcp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-http").Protocol()) + assert.Equal(t, "tcp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-tcp").Protocol()) + assert.Equal(t, "udp", domain.EntrypointName("my-app-production-2fa8domd2sh7ehyqlxf7jvj57xs-app-8080-udp").Protocol()) }) } diff --git a/internal/deployment/domain/state.go b/internal/deployment/domain/state.go deleted file mode 100644 index 95840a82..00000000 --- a/internal/deployment/domain/state.go +++ /dev/null @@ -1,142 +0,0 @@ -package domain - -import ( - "time" - - "github.com/YuukanOO/seelf/pkg/apperr" - "github.com/YuukanOO/seelf/pkg/monad" -) - -var ( - ErrNotInPendingState = apperr.New("not_in_pending_state") - ErrNotInRunningState = apperr.New("not_in_running_state") -) - -const ( - DeploymentStatusPending DeploymentStatus = iota - DeploymentStatusRunning - DeploymentStatusFailed - DeploymentStatusSucceeded -) - -type ( - DeploymentStatus uint8 - - // Holds together information related to the current deployment state. With a value - // object, it is easier to validate consistency between all those related properties. - // The default value represents a pending state. - DeploymentState struct { - status DeploymentStatus - errcode monad.Maybe[string] - services monad.Maybe[Services] - startedAt monad.Maybe[time.Time] - finishedAt monad.Maybe[time.Time] - } -) - -func (s *DeploymentState) Started() error { - if s.status != DeploymentStatusPending { - return ErrNotInPendingState - } - - s.status = DeploymentStatusRunning - s.startedAt.Set(time.Now().UTC()) - - return nil -} - -func (s *DeploymentState) Failed(err error) error { - if s.status != DeploymentStatusRunning { - return ErrNotInRunningState - } - - s.status = DeploymentStatusFailed - s.errcode.Set(err.Error()) - s.finishedAt.Set(time.Now().UTC()) - - return nil -} - -func (s *DeploymentState) Succeeded(services Services) error { - if s.status != DeploymentStatusRunning { - return ErrNotInRunningState - } - - s.status = DeploymentStatusSucceeded - s.services.Set(services) - s.finishedAt.Set(time.Now().UTC()) - - return nil -} - -func (s DeploymentState) Status() DeploymentStatus { return s.status } -func (s DeploymentState) ErrCode() monad.Maybe[string] { return s.errcode } -func (s DeploymentState) Services() monad.Maybe[Services] { return s.services } -func (s DeploymentState) StartedAt() monad.Maybe[time.Time] { return s.startedAt } -func (s DeploymentState) FinishedAt() monad.Maybe[time.Time] { return s.finishedAt } - -const ( - TargetStatusConfiguring TargetStatus = iota - TargetStatusFailed - TargetStatusReady -) - -type ( - TargetStatus uint8 - - TargetState struct { - status TargetStatus - version time.Time - errcode monad.Maybe[string] - lastReadyVersion monad.Maybe[time.Time] // Hold down the last time the target was marked as ready - } -) - -func newTargetState() (t TargetState) { - t.Reconfigure() - return t -} - -// Mark the state as configuring and update the version. -func (t *TargetState) Reconfigure() { - t.status = TargetStatusConfiguring - t.version = time.Now().UTC() - t.errcode.Unset() -} - -// Update the state based on wether or not an error is given and returns a boolean indicating -// if the state has changed. -// -// If there is no error, the target will be considered ready. -// If an error is given, the target will be marked as failed. -// -// In either case, if the state has changed since it has been processed (the version param), -// it will return without doing anything because the result is outdated. -func (t *TargetState) Configured(version time.Time, err error) bool { - if t.IsOutdated(version) { - return false - } - - if err != nil { - t.status = TargetStatusFailed - t.errcode.Set(err.Error()) - return true - } - - t.status = TargetStatusReady - t.lastReadyVersion.Set(version) - t.errcode.Unset() - - return true -} - -// Returns true if the given version is different from the current one or if the one -// provided is already configured. -func (t TargetState) IsOutdated(version time.Time) bool { - return version != t.version || t.status != TargetStatusConfiguring -} - -func (t TargetState) Status() TargetStatus { return t.status } -func (t TargetState) ErrCode() monad.Maybe[string] { return t.errcode } -func (t TargetState) Version() time.Time { return t.version } -func (t TargetState) LastReadyVersion() monad.Maybe[time.Time] { return t.lastReadyVersion } diff --git a/internal/deployment/domain/state_test.go b/internal/deployment/domain/state_test.go deleted file mode 100644 index 39a937f0..00000000 --- a/internal/deployment/domain/state_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package domain_test - -import ( - "errors" - "testing" - - "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" -) - -func Test_DeploymentState(t *testing.T) { - t.Run("should be created in pending state", func(t *testing.T) { - var state domain.DeploymentState - - testutil.Equals(t, domain.DeploymentStatusPending, state.Status()) - testutil.IsFalse(t, state.ErrCode().HasValue()) - testutil.IsFalse(t, state.Services().HasValue()) - testutil.IsFalse(t, state.StartedAt().HasValue()) - testutil.IsFalse(t, state.FinishedAt().HasValue()) - }) - - t.Run("could be marked as started", func(t *testing.T) { - var ( - state domain.DeploymentState - err error - ) - - err = state.Started() - - testutil.IsNil(t, err) - testutil.Equals(t, domain.DeploymentStatusRunning, state.Status()) - testutil.IsTrue(t, state.StartedAt().HasValue()) - testutil.IsFalse(t, state.FinishedAt().HasValue()) - }) - - t.Run("could fail", func(t *testing.T) { - var ( - state domain.DeploymentState - err error - ) - - testutil.IsNil(t, state.Started()) - - err = state.Failed(errors.New("some error")) - - testutil.IsNil(t, err) - testutil.Equals(t, domain.DeploymentStatusFailed, state.Status()) - testutil.Equals(t, "some error", state.ErrCode().MustGet()) - testutil.IsTrue(t, state.StartedAt().HasValue()) - testutil.IsTrue(t, state.FinishedAt().HasValue()) - }) - - t.Run("could succeed", func(t *testing.T) { - var ( - state domain.DeploymentState - err error - ) - - app := must.Panic(domain.NewApp("app1", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true), - "uid")) - conf := must.Panic(app.ConfigSnapshotFor(domain.Production)) - services := domain.Services{ - conf.NewService("app", ""), - } - testutil.IsNil(t, state.Started()) - - err = state.Succeeded(services) - - testutil.IsNil(t, err) - testutil.Equals(t, domain.DeploymentStatusSucceeded, state.Status()) - testutil.IsFalse(t, state.ErrCode().HasValue()) - testutil.IsTrue(t, state.Services().HasValue()) - testutil.DeepEquals(t, services, state.Services().MustGet()) - testutil.IsTrue(t, state.StartedAt().HasValue()) - testutil.IsTrue(t, state.FinishedAt().HasValue()) - }) - - t.Run("should err if trying to start but not in pending state", func(t *testing.T) { - var state domain.DeploymentState - testutil.IsNil(t, state.Started()) - - err := state.Started() - - testutil.ErrorIs(t, domain.ErrNotInPendingState, err) - }) - - t.Run("should err if trying to fail but not in runing state", func(t *testing.T) { - var state domain.DeploymentState - - err := state.Failed(errors.New("an error")) - - testutil.ErrorIs(t, domain.ErrNotInRunningState, err) - }) - - t.Run("should err if trying to succeed but not in runing state", func(t *testing.T) { - var state domain.DeploymentState - - err := state.Succeeded(domain.Services{}) - - testutil.ErrorIs(t, domain.ErrNotInRunningState, err) - }) -} - -func Test_TargetState(t *testing.T) { - t.Run("should be created in configuring state", func(t *testing.T) { - var state domain.TargetState - - testutil.Equals(t, domain.TargetStatusConfiguring, state.Status()) - testutil.IsTrue(t, state.Version().IsZero()) - testutil.IsFalse(t, state.ErrCode().HasValue()) - testutil.IsFalse(t, state.LastReadyVersion().HasValue()) - }) - - t.Run("can be reconfigured", func(t *testing.T) { - var state domain.TargetState - - state.Reconfigure() - - testutil.Equals(t, domain.TargetStatusConfiguring, state.Status()) - testutil.IsFalse(t, state.Version().IsZero()) - testutil.IsFalse(t, state.ErrCode().HasValue()) - }) - - t.Run("could be marked has done and sets the errcode and status appropriately", func(t *testing.T) { - var ( - state domain.TargetState - errFailed = errors.New("failed") - ) - state.Reconfigure() - - testutil.IsTrue(t, state.Configured(state.Version(), errFailed)) - - testutil.Equals(t, domain.TargetStatusFailed, state.Status()) - testutil.Equals(t, errFailed.Error(), state.ErrCode().MustGet()) - testutil.IsFalse(t, state.LastReadyVersion().HasValue()) - - state.Reconfigure() - - testutil.IsTrue(t, state.Configured(state.Version(), nil)) - testutil.Equals(t, state.Version(), state.LastReadyVersion().MustGet()) - - testutil.Equals(t, domain.TargetStatusReady, state.Status()) - testutil.IsFalse(t, state.ErrCode().HasValue()) - }) - - t.Run("should do nothing if the version does not match or if it has been already configured", func(t *testing.T) { - var state domain.TargetState - state.Reconfigure() - - testutil.IsFalse(t, state.Configured(state.Version().Add(-1), nil)) - - testutil.Equals(t, domain.TargetStatusConfiguring, state.Status()) - testutil.IsFalse(t, state.ErrCode().HasValue()) - testutil.IsFalse(t, state.Version().IsZero()) - testutil.IsFalse(t, state.LastReadyVersion().HasValue()) - - state.Configured(state.Version(), nil) - - testutil.IsFalse(t, state.Configured(state.Version(), errors.New("should not happen"))) - - testutil.Equals(t, domain.TargetStatusReady, state.Status()) - testutil.Equals(t, state.Version(), state.LastReadyVersion().MustGet()) - testutil.IsFalse(t, state.ErrCode().HasValue()) - testutil.IsFalse(t, state.Version().IsZero()) - }) -} diff --git a/internal/deployment/domain/target.go b/internal/deployment/domain/target.go index 149a3310..c09af041 100644 --- a/internal/deployment/domain/target.go +++ b/internal/deployment/domain/target.go @@ -28,7 +28,13 @@ var ( const ( CleanupStrategyDefault CleanupStrategy = iota // Default strategy, try to remove the target data but returns an error if it fails - CleanupStrategySkip // Skip the cleanup because no resource has been deployed + CleanupStrategySkip // Skip the cleanup because no resource has been deployed or we can't remove them anymore +) + +const ( + TargetStatusConfiguring TargetStatus = iota + TargetStatusFailed + TargetStatusReady ) type ( @@ -43,7 +49,7 @@ type ( id TargetID name string - url Url + url monad.Maybe[Url] provider ProviderConfig state TargetState customEntrypoints TargetEntrypoints @@ -67,7 +73,6 @@ type ( ID TargetID Name string - Url Url Provider ProviderConfig State TargetState Entrypoints TargetEntrypoints @@ -95,6 +100,12 @@ type ( Url Url } + TargetUrlRemoved struct { + bus.Notification + + ID TargetID + } + TargetProviderChanged struct { bus.Notification @@ -127,6 +138,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 +151,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 +163,6 @@ func NewTarget( t.apply(TargetCreated{ ID: id.New[TargetID](), Name: name, - Url: url.Root(), Provider: provider, State: newTargetState(), Entrypoints: make(TargetEntrypoints), @@ -229,8 +233,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 +245,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() @@ -324,7 +350,7 @@ func (t *Target) Reconfigure() error { // Mark the target (in the given version) has configured (by an external system). // If the given version does not match the current one, nothing will be done. func (t *Target) Configured(version time.Time, assigned TargetEntrypointsAssigned, err error) { - if !t.state.Configured(version, err) { + if !t.state.configured(version, err) { return } @@ -347,7 +373,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,18 +487,19 @@ 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 } // Returns true if the given configuration version is different from the current one. func (t *Target) IsOutdated(version time.Time) bool { - return t.state.IsOutdated(version) + return t.state.isOutdated(version) } func (t *Target) reconfigure() { - t.state.Reconfigure() + t.state.reconfigure() t.apply(TargetStateChanged{ ID: t.id, @@ -486,6 +513,10 @@ func (t *Target) raiseEntrypointsChangedAndReconfigure() { Entrypoints: t.customEntrypoints, }) + if t.IsManual() { + return + } + t.reconfigure() } @@ -494,7 +525,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 +532,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: @@ -516,6 +548,66 @@ func (t *Target) apply(e event.Event) { event.Store(t, e) } +type ( + TargetStatus uint8 + + TargetState struct { + status TargetStatus + version time.Time + errcode monad.Maybe[string] + lastReadyVersion monad.Maybe[time.Time] // Hold down the last time the target was marked as ready + } +) + +func newTargetState() (t TargetState) { + t.reconfigure() + return t +} + +// Mark the state as configuring and update the version. +func (t *TargetState) reconfigure() { + t.status = TargetStatusConfiguring + t.version = time.Now().UTC() + t.errcode.Unset() +} + +// Update the state based on wether or not an error is given and returns a boolean indicating +// if the state has changed. +// +// If there is no error, the target will be considered ready. +// If an error is given, the target will be marked as failed. +// +// In either case, if the state has changed since it has been processed (the version param), +// it will return without doing anything because the result is outdated. +func (t *TargetState) configured(version time.Time, err error) bool { + if t.isOutdated(version) { + return false + } + + if err != nil { + t.status = TargetStatusFailed + t.errcode.Set(err.Error()) + return true + } + + t.status = TargetStatusReady + t.lastReadyVersion.Set(version) + t.errcode.Unset() + + return true +} + +// Returns true if the given version is different from the current one or if the one +// provided is already configured. +func (t TargetState) isOutdated(version time.Time) bool { + return version != t.version || t.status != TargetStatusConfiguring +} + +func (t TargetState) Status() TargetStatus { return t.status } +func (t TargetState) ErrCode() monad.Maybe[string] { return t.errcode } +func (t TargetState) Version() time.Time { return t.version } +func (t TargetState) LastReadyVersion() monad.Maybe[time.Time] { return t.lastReadyVersion } + func (e TargetEntrypoints) Value() (driver.Value, error) { return storage.ValueJSON(e) } func (e *TargetEntrypoints) Scan(value any) error { return storage.ScanJSON(value, e) } diff --git a/internal/deployment/domain/target_test.go b/internal/deployment/domain/target_test.go index d17ec11d..1fcf3269 100644 --- a/internal/deployment/domain/target_test.go +++ b/internal/deployment/domain/target_test.go @@ -7,675 +7,875 @@ import ( auth "github.com/YuukanOO/seelf/internal/auth/domain" "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" + shared "github.com/YuukanOO/seelf/pkg/domain" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_Target(t *testing.T) { - var ( - name = "my-target" - targetUrl = must.Panic(domain.UrlFrom("http://my-url.com")) - config domain.ProviderConfig = dummyProviderConfig{} - uid auth.UserID = "uid" - - urlNotUnique = domain.NewTargetUrlRequirement(targetUrl, false) - urlUnique = domain.NewTargetUrlRequirement(targetUrl, true) - configNotUnique = domain.NewProviderConfigRequirement(config, false) - configUnique = domain.NewProviderConfigRequirement(config, true) - app = must.Panic(domain.NewApp("my-app", - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("production-target"), true, true), - domain.NewEnvironmentConfigRequirement(domain.NewEnvironmentConfig("staging-target"), true, true), - "uid")) - deployConfig = must.Panic(app.ConfigSnapshotFor(domain.Production)) - ) - - t.Run("should fail if the url is not unique", func(t *testing.T) { - _, err := domain.NewTarget(name, urlNotUnique, configUnique, uid) - testutil.Equals(t, domain.ErrUrlAlreadyTaken, err) - }) - - t.Run("should fail if the config is not unique", func(t *testing.T) { - _, err := domain.NewTarget(name, urlUnique, configNotUnique, uid) - testutil.Equals(t, domain.ErrConfigAlreadyTaken, err) - }) - - t.Run("should be instantiable", func(t *testing.T) { - target, err := domain.NewTarget(name, urlUnique, configUnique, uid) - - testutil.IsNil(t, err) - testutil.HasNEvents(t, &target, 1) - evt := testutil.EventIs[domain.TargetCreated](t, &target, 0) - - testutil.NotEquals(t, "", evt.ID) - testutil.Equals(t, name, evt.Name) - testutil.Equals(t, targetUrl.String(), evt.Url.String()) - testutil.Equals(t, config, evt.Provider) - testutil.Equals(t, domain.TargetStatusConfiguring, evt.State.Status()) - testutil.Equals(t, uid, evt.Created.By()) - }) - - t.Run("could be renamed and raise the event only if different", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - - err := target.Rename("new-name") - - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.TargetRenamed](t, &target, 1) - testutil.Equals(t, "new-name", evt.Name) - - testutil.IsNil(t, target.Rename("new-name")) - testutil.HasNEvents(t, &target, 2) - }) - - t.Run("could not be renamed if delete requested", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) - - testutil.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 := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - newUrl := must.Panic(domain.UrlFrom("http://new-url.com")) - err := target.HasUrl(domain.NewTargetUrlRequirement(newUrl, false)) - - testutil.ErrorIs(t, domain.ErrUrlAlreadyTaken, err) - - err = target.HasUrl(domain.NewTargetUrlRequirement(newUrl, true)) - - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.TargetUrlChanged](t, &target, 1) - testutil.Equals(t, newUrl.String(), evt.Url.String()) - - evtTargetChanged := testutil.EventIs[domain.TargetStateChanged](t, &target, 2) - testutil.Equals(t, domain.TargetStatusConfiguring, evtTargetChanged.State.Status()) - - testutil.IsNil(t, target.HasUrl(domain.NewTargetUrlRequirement(newUrl, true))) - testutil.HasNEvents(t, &target, 3) - }) - - t.Run("could not have its domain changed if delete requested", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - - newUrl := domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://new-url.com")), true) - - testutil.IsNil(t, target.RequestCleanup(false, uid)) - testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, target.HasUrl(newUrl)) - }) - - t.Run("should forbid a provider change if the fingerprint has changed", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true), configUnique, uid)) - - err := target.HasProvider(domain.NewProviderConfigRequirement(dummyProviderConfig{data: "new-config", fingerprint: "new-fingerprint"}, true)) - - testutil.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) { - target := must.Panic(domain.NewTarget(name, - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://docker.localhost")), true), - configUnique, uid)) - newConfig := dummyProviderConfig{data: "new-config"} - - err := target.HasProvider(domain.NewProviderConfigRequirement(newConfig, false)) - - testutil.ErrorIs(t, domain.ErrConfigAlreadyTaken, err) - - err = target.HasProvider(domain.NewProviderConfigRequirement(newConfig, true)) - - testutil.IsNil(t, err) - evt := testutil.EventIs[domain.TargetProviderChanged](t, &target, 1) - testutil.IsTrue(t, newConfig == evt.Provider) - - evtTargetChanged := testutil.EventIs[domain.TargetStateChanged](t, &target, 2) - testutil.Equals(t, domain.TargetStatusConfiguring, evtTargetChanged.State.Status()) + // Common data used for custom entrypoints exposure + deployment := fixture.Deployment() + builder := deployment.Config().ServicesBuilder() + app := builder.AddService("app", "app-image") + app.AddHttpEntrypoint(80, false) + http := app.AddHttpEntrypoint(3000, true) + db := builder.AddService("db", "db-image") + tcp := db.AddTCPEntrypoint(5432, true) + + 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.Equal(t, target.CurrentVersion(), created.State.Version()) + assert.Zero(t, created.State.ErrCode()) + assert.Zero(t, created.State.LastReadyVersion()) + }) + }) + + 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 true if the target is not in a configuring state", func(t *testing.T) { + target := fixture.Target() + target.Configured(target.CurrentVersion(), nil, nil) + + assert.True(t, target.IsOutdated(target.CurrentVersion())) + }) + + 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()) + + 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()) + }) - testutil.IsNil(t, target.HasProvider(domain.NewProviderConfigRequirement(newConfig, true))) - testutil.HasNEvents(t, &target, 3) - }) - - t.Run("could not have its provider changed if delete requested", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - - testutil.IsNil(t, target.RequestCleanup(false, uid)) - testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, target.HasProvider(configUnique)) - }) - - t.Run("could be marked as configured and raise the appropriate event", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - - target.Configured(target.CurrentVersion().Add(-1*time.Hour), nil, nil) - - testutil.HasNEvents(t, &target, 1) - testutil.EventIs[domain.TargetCreated](t, &target, 0) - - target.Configured(target.CurrentVersion(), nil, nil) - target.Configured(target.CurrentVersion(), nil, nil) // Should not raise a new event - - testutil.HasNEvents(t, &target, 2) - changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 1) - testutil.Equals(t, domain.TargetStatusReady, changed.State.Status()) - }) - - t.Run("should handle entrypoints assignment on configuration", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + 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() - // Assigning non existing entrypoints should just be ignored - target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{ - app.ID(): { - 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.Service, db.Service}) + + 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.Service, db.Service}) + + 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.Service, db.Service}) + + target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service}) + + 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.Service, db.Service}) + + target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service}) + + 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.Service, db.Service}) + + target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service}) + 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.Service, db.Service}) + + assert.HasNEvents(t, 3, &target) + }) + }) + + t.Run("could be marked as configured", func(t *testing.T) { + t.Run("should do nothing if the version does not match", func(t *testing.T) { + target := fixture.Target() + target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service}) + + target.Configured(target.CurrentVersion().Add(-1*time.Second), domain.TargetEntrypointsAssigned{ + deployment.Config().AppID(): { + domain.Production: { + http.Name(): 3000, + tcp.Name(): 3001, + }, }, - }, - }, nil) + }, nil) - testutil.HasNEvents(t, &target, 2) - testutil.DeepEquals(t, domain.TargetEntrypoints{}, target.CustomEntrypoints()) + assert.HasNEvents(t, 2, &target) + }) - dbService := deployConfig.NewService("db", "postgres:14-alpine") - http := dbService.AddHttpEntrypoint(deployConfig, 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(app.ID(), 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{ - app.ID(): { - domain.Production: { - http.Name(): 8081, - tcp.Name(): 8082, - }, - }, - }, errors.New("some error")) - - testutil.HasNEvents(t, &target, 5) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { - 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()) + }) - testutil.IsNil(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.Service, db.Service}) + err := errors.New("an error") - // No error, should update the entrypoints correctly - target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{ - app.ID(): { - 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("")) + assert.Zero(t, stateChanged.State.LastReadyVersion()) + }) + + 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.Service, db.Service}) + target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app.Service, db.Service}) + + 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) - - testutil.HasNEvents(t, &target, 8) - testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 6) - changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 7) - testutil.Equals(t, domain.TargetStatusReady, changed.State.Status()) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { - 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 := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - dbService := deployConfig.NewService("db", "postgres:14-alpine") - http := dbService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{}) - tcp := dbService.AddTCPEntrypoint(5432) - - target.UnExposeEntrypoints(app.ID()) - - testutil.HasNEvents(t, &target, 1) - - target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{dbService}) - target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{ - app.ID(): { - 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()) + assert.Zero(t, stateChanged.State.ErrCode()) + assert.Equal(t, target.CurrentVersion(), stateChanged.State.LastReadyVersion().Get(time.Time{})) + }) + }) + + 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.Service, db.Service}) + target.ExposeEntrypoints(deployment.Config().AppID(), domain.Production, domain.Services{app.Service, db.Service}) + target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app.Service, db.Service}) + 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(app.ID()) - - testutil.HasNEvents(t, &target, 7) - testutil.DeepEquals(t, domain.TargetEntrypoints{}, target.CustomEntrypoints()) - changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 6) - testutil.Equals(t, domain.TargetStatusConfiguring, changed.State.Status()) - - target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{dbService}) - target.Configured(target.CurrentVersion(), domain.TargetEntrypointsAssigned{ - app.ID(): { - 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.Service, db.Service}) + target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app.Service, db.Service}) + 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.Service, db.Service}) + target.ExposeEntrypoints(deployment.Config().AppID(), domain.Staging, domain.Services{app.Service, db.Service}) + 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.Service, db.Service}) + 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(app.ID(), domain.Staging) - target.UnExposeEntrypoints(app.ID(), domain.Production) + target.UnExposeEntrypoints(deployment.Config().AppID(), domain.Production) - testutil.HasNEvents(t, &target, 13) - testutil.DeepEquals(t, domain.TargetEntrypoints{}, target.CustomEntrypoints()) - changed = testutil.EventIs[domain.TargetStateChanged](t, &target, 12) - testutil.Equals(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 := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - - // Configuring - err := target.CheckAvailability() - - testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) - - // Configuration failed - target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed")) - - err = target.CheckAvailability() - - testutil.ErrorIs(t, domain.ErrTargetConfigurationFailed, err) - - // Configuration success - target.Reconfigure() - - target.Configured(target.CurrentVersion(), nil, nil) + t.Run("when configuring", func(t *testing.T) { + target := fixture.Target() - err = target.CheckAvailability() + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.CheckAvailability()) + }) - testutil.IsNil(t, err) + t.Run("when configuration failed", func(t *testing.T) { + target := fixture.Target() + target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed")) - // Delete requested - target.RequestCleanup(false, uid) + assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, target.CheckAvailability()) + }) - err = target.CheckAvailability() + t.Run("when ready", func(t *testing.T) { + target := fixture.Target() + target.Configured(target.CurrentVersion(), nil, nil) - testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, err) - }) + assert.Nil(t, target.CheckAvailability()) + }) - t.Run("could not be reconfigured if cleanup requested", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) + 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")) - testutil.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Reconfigure()) + assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.CheckAvailability()) + }) }) - t.Run("could not be reconfigured if configuring", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, 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() - testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.Reconfigure()) - }) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.Reconfigure()) + }) - t.Run("should not be removed if still used by an app", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) + 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")) - testutil.ErrorIs(t, domain.ErrTargetInUse, target.RequestCleanup(true, uid)) - }) + assert.ErrorIs(t, domain.ErrTargetCleanupRequested, target.Reconfigure()) + }) - t.Run("should not be removed if configuring", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - - testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.RequestCleanup(false, uid)) - }) + t.Run("should succeed otherwise", func(t *testing.T) { + target := fixture.Target() + target.Configured(target.CurrentVersion(), nil, nil) - t.Run("could be removed if no app is using it", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) + assert.Nil(t, target.Reconfigure()) - err := target.RequestCleanup(false, uid) - testutil.IsNil(t, err) - - testutil.IsNil(t, err) - testutil.HasNEvents(t, &target, 3) - evt := testutil.EventIs[domain.TargetCleanupRequested](t, &target, 2) - testutil.Equals(t, target.ID(), evt.ID) + assert.HasNEvents(t, 3, &target) + stateChanged := assert.EventIs[domain.TargetStateChanged](t, &target, 2) + assert.Equal(t, domain.TargetStatusConfiguring, stateChanged.State.Status()) + }) }) - t.Run("should not raise an event is the target is already marked has deleting", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - - testutil.IsNil(t, target.RequestCleanup(false, uid)) - testutil.IsNil(t, target.RequestCleanup(false, uid)) - - testutil.HasNEvents(t, &target, 3) - }) + 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() - t.Run("should returns an err if trying to cleanup a target while configuring", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + assert.ErrorIs(t, domain.ErrTargetInUse, target.RequestCleanup(true, "uid")) + }) - _, err := target.CleanupStrategy(false) + t.Run("should returns an err if configuring", func(t *testing.T) { + target := fixture.Target() - testutil.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) - }) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, target.RequestCleanup(false, "uid")) + }) - t.Run("should returns an err if trying to cleanup a target while deployments are still running", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) + t.Run("should succeed otherwise", func(t *testing.T) { + target := fixture.Target() + target.Configured(target.CurrentVersion(), nil, nil) - _, err := target.CleanupStrategy(true) + assert.Nil(t, target.RequestCleanup(false, "uid")) - testutil.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err) - }) + 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 the skip cleanup strategy if the configuration has failed and the target could not be updated anymore", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - target.Reconfigure() - target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed")) - target.RequestCleanup(false, uid) + 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")) - s, err := target.CleanupStrategy(false) + assert.Nil(t, target.RequestCleanup(false, "uid")) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategySkip, s) + assert.HasNEvents(t, 3, &target) + }) }) - t.Run("should returns the skip cleanup strategy if the configuration has failed and has never been reachable", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed")) + 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() - s, err := target.CleanupStrategy(false) + _, err := target.CleanupStrategy(true) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategySkip, s) - }) + assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err) + }) - t.Run("should returns an err if the configuration has failed but the target is still updatable", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - 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.CleanupStrategy(false) + _, err := target.CleanupStrategy(false) - testutil.ErrorIs(t, domain.ErrTargetConfigurationFailed, err) - }) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) + }) - t.Run("should returns the default strategy if the target is correctly configured", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) + 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) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategyDefault, s) - }) + assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err) + }) - t.Run("returns an err if trying to cleanup an app while configuring", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + 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.AppCleanupStrategy(false, true) + strategy, err := target.CleanupStrategy(false) - testutil.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 := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) + 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, false) + strategy, err := target.CleanupStrategy(false) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategySkip, s) + assert.Nil(t, err) + assert.Equal(t, domain.CleanupStrategyDefault, strategy) + }) }) - t.Run("returns a skip strategy when trying to cleanup an app when no successful deployment has been made", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + 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")) - s, err := target.AppCleanupStrategy(false, false) + strategy, err := target.AppCleanupStrategy(false, true) - testutil.IsNil(t, err) - testutil.Equals(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 := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - target.Reconfigure() - target.Configured(target.CurrentVersion(), nil, errors.New("configuration failed")) + 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() - _, err := target.AppCleanupStrategy(false, true) + _, err := target.AppCleanupStrategy(true, true) - testutil.ErrorIs(t, domain.ErrTargetConfigurationFailed, err) - }) + assert.ErrorIs(t, domain.ErrRunningOrPendingDeployments, 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 := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) + 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() - _, err := target.AppCleanupStrategy(true, false) + strategy, err := target.AppCleanupStrategy(false, false) - testutil.ErrorIs(t, domain.ErrRunningOrPendingDeployments, err) - }) + assert.Nil(t, err) + assert.Equal(t, domain.CleanupStrategySkip, strategy) + }) - t.Run("returns a default strategy when trying to remove an app and everything is good to process it", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) + t.Run("should returns an error if the target is being configured", func(t *testing.T) { + target := fixture.Target() - s, err := target.AppCleanupStrategy(false, true) + _, err := target.AppCleanupStrategy(false, true) - testutil.IsNil(t, err) - testutil.Equals(t, domain.CleanupStrategyDefault, s) - }) + assert.ErrorIs(t, domain.ErrTargetConfigurationInProgress, err) + }) - t.Run("should do nothing if trying to expose an empty entrypoints array", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) + 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")) - target.ExposeEntrypoints(app.ID(), domain.Production, domain.Services{}) - testutil.HasNEvents(t, &target, 1) + _, err := target.AppCleanupStrategy(false, true) - target.ExposeEntrypoints(app.ID(), domain.Production, nil) - testutil.HasNEvents(t, &target, 1) - }) + assert.ErrorIs(t, domain.ErrTargetConfigurationFailed, err) + }) - t.Run("should switch to the configuring state if adding new entrypoints to expose", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - appService := deployConfig.NewService("app", "") - http := appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{}) - udp := appService.AddUDPEntrypoint(8080) - dbService := deployConfig.NewService("db", "postgres:14-alpine") - tcp := dbService.AddTCPEntrypoint(5432) - - services := domain.Services{appService, dbService} - - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), services) - - testutil.HasNEvents(t, &target, 3) - evt := testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 1) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { - deployConfig.Environment(): { - http.Name(): monad.None[domain.Port](), - udp.Name(): monad.None[domain.Port](), - tcp.Name(): monad.None[domain.Port](), - }, - }, - }, evt.Entrypoints) + 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) - changed := testutil.EventIs[domain.TargetStateChanged](t, &target, 2) - testutil.Equals(t, domain.TargetStatusConfiguring, changed.State.Status()) + strategy, err := target.AppCleanupStrategy(false, true) - // Should not trigger it again - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), services) - testutil.HasNEvents(t, &target, 3) + assert.Nil(t, err) + assert.Equal(t, domain.CleanupStrategyDefault, strategy) + }) }) - t.Run("should switch to the configuring state if adding new entrypoints to an already exposed environment", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - appService := deployConfig.NewService("app", "") + 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() - http := appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{}) + assert.ErrorIs(t, domain.ErrTargetCleanupNeeded, target.Delete(true)) + }) - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService}) + t.Run("should returns an error if the target resources has not been cleaned up", func(t *testing.T) { + target := fixture.Target() - testutil.HasNEvents(t, &target, 3) - evt := testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 1) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { - deployConfig.Environment(): { - http.Name(): monad.None[domain.Port](), - }, - }, - }, evt.Entrypoints) - - // Adding a new entrypoint should trigger new events - dbService := deployConfig.NewService("db", "postgres:14-alpine") - tcp := dbService.AddTCPEntrypoint(5432) + assert.ErrorIs(t, domain.ErrTargetCleanupNeeded, target.Delete(false)) + }) - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService, dbService}) - - testutil.HasNEvents(t, &target, 5) - evt = testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 3) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { - deployConfig.Environment(): { - http.Name(): monad.None[domain.Port](), - tcp.Name(): monad.None[domain.Port](), - }, - }, - }, evt.Entrypoints) + 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")) - // Again with the same entrypoints, should trigger nothing new - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService, dbService, deployConfig.NewService("cache", "redis:6-alpine")}) - testutil.HasNEvents(t, &target, 5) + 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 removing entrypoints", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - appService := deployConfig.NewService("app", "") - - http := appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{}) - appService.AddUDPEntrypoint(8080) - dbService := deployConfig.NewService("db", "postgres:14-alpine") - tcp := dbService.AddTCPEntrypoint(5432) - - target.ExposeEntrypoints(app.ID(), deployConfig.Environment(), domain.Services{appService, dbService}) +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) - // Let's remove the UDP entrypoint - appService = deployConfig.NewService("app", "") - appService.AddHttpEntrypoint(deployConfig, 80, domain.HttpEntrypointOptions{}) + evt := assert.EventIs[domain.TargetStateChanged](t, &target, 1) + assert.False(t, evt.WentToConfiguringState()) + }) - target.ExposeEntrypoints(app.ID(), deployConfig.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()) - testutil.HasNEvents(t, &target, 5) - evt := testutil.EventIs[domain.TargetEntrypointsChanged](t, &target, 3) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { - deployConfig.Environment(): { - http.Name(): monad.None[domain.Port](), - tcp.Name(): monad.None[domain.Port](), - }, - }, - }, evt.Entrypoints) + evt := assert.EventIs[domain.TargetStateChanged](t, &target, 2) + assert.True(t, evt.WentToConfiguringState()) + }) }) +} - t.Run("should remove empty map keys when updating entrypoints", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - - appService := deployConfig.NewService("app", "") +func Test_TargetEntrypointsAssigned(t *testing.T) { + t.Run("should provide a function to set entrypoints values", func(t *testing.T) { + assigned := make(domain.TargetEntrypointsAssigned) - http := appService.AddHttpEntrypoint(deployConfig, 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(app.ID(), deployConfig.Environment(), domain.Services{appService}) - testutil.DeepEquals(t, domain.TargetEntrypoints{ - app.ID(): { + 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(app.ID(), deployConfig.Environment(), domain.Services{}) - - testutil.DeepEquals(t, domain.TargetEntrypoints{}, target.CustomEntrypoints()) - }) - - t.Run("should not be removed if no cleanup request has been set", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - - err := target.Delete(true) - - testutil.ErrorIs(t, domain.ErrTargetCleanupNeeded, err) - }) - - t.Run("should not be removed if target resources have not been cleaned up", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) // No application is using it - - err := target.Delete(false) - - testutil.ErrorIs(t, domain.ErrTargetCleanupNeeded, err) - }) - - t.Run("could be removed if resources have been cleaned up", func(t *testing.T) { - target := must.Panic(domain.NewTarget(name, urlUnique, configUnique, uid)) - target.Configured(target.CurrentVersion(), nil, nil) - testutil.IsNil(t, target.RequestCleanup(false, uid)) - - err := target.Delete(true) - - testutil.IsNil(t, err) - testutil.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 := must.Panic(domain.NewTarget("my-target", - domain.NewTargetUrlRequirement(must.Panic(domain.UrlFrom("http://my-url.com")), true), - domain.NewProviderConfigRequirement(dummyProviderConfig{}, true), "uid", - )) - target.Configured(target.CurrentVersion(), nil, nil) - - evt := testutil.EventIs[domain.TargetStateChanged](t, &target, 1) - testutil.IsFalse(t, evt.WentToConfiguringState()) - - testutil.IsNil(t, target.Reconfigure()) - - evt = testutil.EventIs[domain.TargetStateChanged](t, &target, 2) - testutil.IsTrue(t, evt.WentToConfiguringState()) + }, assigned) }) } - -type dummyProviderConfig struct { - data string - fingerprint string -} - -func (d dummyProviderConfig) Kind() string { return "dummy" } -func (d dummyProviderConfig) Fingerprint() string { return d.fingerprint } -func (d dummyProviderConfig) String() string { return d.fingerprint } - -func (d dummyProviderConfig) Equals(other domain.ProviderConfig) bool { - return d == other -} diff --git a/internal/deployment/domain/url_test.go b/internal/deployment/domain/url_test.go index b3d7fe7b..f69bcb9a 100644 --- a/internal/deployment/domain/url_test.go +++ b/internal/deployment/domain/url_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_Url(t *testing.T) { @@ -26,37 +26,37 @@ func Test_Url(t *testing.T) { u, err := domain.UrlFrom(test.value) if test.valid { - testutil.IsNil(t, err) - testutil.Equals(t, test.value, u.String()) + assert.Nil(t, err) + assert.Equal(t, test.value, u.String()) } else { - testutil.ErrorIs(t, domain.ErrInvalidUrl, err) + assert.ErrorIs(t, domain.ErrInvalidUrl, err) } }) } }) t.Run("should get wether its a secure url or not", func(t *testing.T) { - httpUrl, _ := domain.UrlFrom("http://something.com") - httpsUrl, _ := domain.UrlFrom("https://something.com") + httpUrl := must.Panic(domain.UrlFrom("http://something.com")) + httpsUrl := must.Panic(domain.UrlFrom("https://something.com")) - testutil.IsFalse(t, httpUrl.UseSSL()) - testutil.IsTrue(t, httpsUrl.UseSSL()) + assert.False(t, httpUrl.UseSSL()) + assert.True(t, httpsUrl.UseSSL()) }) t.Run("should be able to prepend a subdomain", func(t *testing.T) { - url, _ := domain.UrlFrom("http://something.com") + url := must.Panic(domain.UrlFrom("http://something.com")) subdomained := url.SubDomain("an-app") - testutil.Equals(t, "http://something.com", url.String()) - testutil.Equals(t, "http://an-app.something.com", subdomained.String()) + assert.Equal(t, "http://something.com", url.String()) + assert.Equal(t, "http://an-app.something.com", subdomained.String()) }) t.Run("should implement the valuer interface", func(t *testing.T) { - url, _ := domain.UrlFrom("http://something.com") + url := must.Panic(domain.UrlFrom("http://something.com")) value, err := url.Value() - testutil.IsNil(t, err) - testutil.Equals(t, "http://something.com", value.(string)) + assert.Nil(t, err) + assert.Equal(t, "http://something.com", value.(string)) }) t.Run("should implement the scanner interface", func(t *testing.T) { @@ -66,16 +66,16 @@ func Test_Url(t *testing.T) { ) err := url.Scan(value) - testutil.IsNil(t, err) - testutil.Equals(t, "http://something.com", url.String()) + assert.Nil(t, err) + assert.Equal(t, "http://something.com", url.String()) }) t.Run("should marshal to json", func(t *testing.T) { - url, _ := domain.UrlFrom("http://something.com") + url := must.Panic(domain.UrlFrom("http://something.com")) json, err := url.MarshalJSON() - testutil.IsNil(t, err) - testutil.Equals(t, `"http://something.com"`, string(json)) + assert.Nil(t, err) + assert.Equal(t, `"http://something.com"`, string(json)) }) t.Run("should unmarshal from json", func(t *testing.T) { @@ -85,36 +85,36 @@ func Test_Url(t *testing.T) { ) err := url.UnmarshalJSON([]byte(value)) - testutil.IsNil(t, err) - testutil.Equals(t, "http://something.com", url.String()) + assert.Nil(t, err) + assert.Equal(t, "http://something.com", url.String()) }) t.Run("should retrieve the user part of an url if any", func(t *testing.T) { url := must.Panic(domain.UrlFrom("http://seelf@docker.localhost")) - testutil.IsTrue(t, url.User().HasValue()) - testutil.Equals(t, "seelf", url.User().MustGet()) + assert.True(t, url.User().HasValue()) + assert.Equal(t, "seelf", url.User().MustGet()) url = must.Panic(domain.UrlFrom("http://docker.localhost")) - testutil.IsFalse(t, url.User().HasValue()) + assert.False(t, url.User().HasValue()) }) t.Run("should be able to remove the user part of an url", func(t *testing.T) { url := must.Panic(domain.UrlFrom("http://seelf@docker.localhost")) - testutil.Equals(t, "http://docker.localhost", url.WithoutUser().String()) - testutil.Equals(t, "http://seelf@docker.localhost", url.String()) + assert.Equal(t, "http://docker.localhost", url.WithoutUser().String()) + assert.Equal(t, "http://seelf@docker.localhost", url.String()) url = must.Panic(domain.UrlFrom("http://docker.localhost")) - testutil.Equals(t, "http://docker.localhost", url.WithoutUser().String()) + assert.Equal(t, "http://docker.localhost", url.WithoutUser().String()) }) t.Run("should be able to remove path and query from an url", func(t *testing.T) { url := must.Panic(domain.UrlFrom("http://docker.localhost/some/path?query=value")) - testutil.Equals(t, "http://docker.localhost", url.Root().String()) - testutil.Equals(t, "http://docker.localhost/some/path?query=value", url.String()) + assert.Equal(t, "http://docker.localhost", url.Root().String()) + assert.Equal(t, "http://docker.localhost/some/path?query=value", url.String()) }) } diff --git a/internal/deployment/domain/version_control_test.go b/internal/deployment/domain/version_control_test.go index 659c0deb..9fc77b61 100644 --- a/internal/deployment/domain/version_control_test.go +++ b/internal/deployment/domain/version_control_test.go @@ -1,11 +1,10 @@ package domain_test import ( - "fmt" "testing" "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" ) func Test_VersionControl(t *testing.T) { @@ -14,8 +13,8 @@ func Test_VersionControl(t *testing.T) { conf := domain.NewVersionControl(url) - testutil.Equals(t, url, conf.Url()) - testutil.IsFalse(t, conf.Token().HasValue()) + assert.Equal(t, url, conf.Url()) + assert.False(t, conf.Token().HasValue()) }) t.Run("should hold a token if authentication is needed", func(t *testing.T) { @@ -27,8 +26,8 @@ func Test_VersionControl(t *testing.T) { conf := domain.NewVersionControl(url) conf.Authenticated(token) - testutil.Equals(t, url, conf.Url()) - testutil.Equals(t, token, conf.Token().Get("")) + assert.Equal(t, url, conf.Url()) + assert.Equal(t, token, conf.Token().Get("")) }) t.Run("could update the url", func(t *testing.T) { @@ -42,8 +41,8 @@ func Test_VersionControl(t *testing.T) { conf.Authenticated(token) conf.HasUrl(newUrl) - testutil.Equals(t, newUrl, conf.Url()) - testutil.Equals(t, token, conf.Token().Get("")) + assert.Equal(t, newUrl, conf.Url()) + assert.Equal(t, token, conf.Token().Get("")) }) t.Run("could remove a token", func(t *testing.T) { @@ -53,87 +52,7 @@ func Test_VersionControl(t *testing.T) { conf.Authenticated("a token") conf.Public() - testutil.Equals(t, url, conf.Url()) - testutil.IsFalse(t, conf.Token().HasValue()) - }) - - t.Run("should be able to compare itself with another config", func(t *testing.T) { - var ( - url, _ = domain.UrlFrom("http://somewhere.git") - sameUrlDifferentStruct, _ = domain.UrlFrom("http://somewhere.git") - anotherUrl, _ = domain.UrlFrom("http://somewhere-else.git") - token string = "some token" - anotherToken string = "another token" - ) - - tests := []struct { - first func() domain.VersionControl - second func() domain.VersionControl - expected bool - }{ - { - func() domain.VersionControl { - conf := domain.NewVersionControl(url) - conf.Authenticated(token) - return conf - }, - func() domain.VersionControl { - return domain.NewVersionControl(sameUrlDifferentStruct) - }, - false, - }, - { - func() domain.VersionControl { - return domain.NewVersionControl(url) - }, - func() domain.VersionControl { - return domain.NewVersionControl(anotherUrl) - }, - false, - }, - { - func() domain.VersionControl { - conf := domain.NewVersionControl(url) - conf.Authenticated(token) - return conf - }, - func() domain.VersionControl { - conf := domain.NewVersionControl(sameUrlDifferentStruct) - conf.Authenticated(anotherToken) - return conf - }, - false, - }, - { - func() domain.VersionControl { - return domain.NewVersionControl(url) - }, - func() domain.VersionControl { - return domain.NewVersionControl(sameUrlDifferentStruct) - }, - true, - }, - { - func() domain.VersionControl { - conf := domain.NewVersionControl(url) - conf.Authenticated(token) - return conf - }, - func() domain.VersionControl { - conf := domain.NewVersionControl(sameUrlDifferentStruct) - conf.Authenticated(token) - return conf - }, - true, - }, - } - - for _, tt := range tests { - f := tt.first() - s := tt.second() - t.Run(fmt.Sprintf("%v %v", f, s), func(t *testing.T) { - testutil.Equals(t, tt.expected, f == s) - }) - } + assert.Equal(t, url, conf.Url()) + assert.False(t, conf.Token().HasValue()) }) } diff --git a/internal/deployment/fixture/app.go b/internal/deployment/fixture/app.go new file mode 100644 index 00000000..965f6dd2 --- /dev/null +++ b/internal/deployment/fixture/app.go @@ -0,0 +1,65 @@ +//go:build !release + +package fixture + +import ( + auth "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/pkg/id" + "github.com/YuukanOO/seelf/pkg/must" +) + +type ( + appOption struct { + name domain.AppName + production domain.EnvironmentConfig + staging domain.EnvironmentConfig + createdBy auth.UserID + } + + AppOptionBuilder func(*appOption) +) + +func App(options ...AppOptionBuilder) domain.App { + opts := appOption{ + name: id.New[domain.AppName](), + production: domain.NewEnvironmentConfig(id.New[domain.TargetID]()), + staging: domain.NewEnvironmentConfig(id.New[domain.TargetID]()), + createdBy: id.New[auth.UserID](), + } + + for _, o := range options { + o(&opts) + } + + return must.Panic(domain.NewApp(opts.name, + domain.NewEnvironmentConfigRequirement(opts.production, true, true), + domain.NewEnvironmentConfigRequirement(opts.staging, true, true), + opts.createdBy, + )) +} + +func WithAppName(name domain.AppName) AppOptionBuilder { + return func(o *appOption) { + o.name = name + } +} + +func WithAppCreatedBy(uid auth.UserID) AppOptionBuilder { + return func(o *appOption) { + o.createdBy = uid + } +} + +func WithProductionConfig(production domain.EnvironmentConfig) AppOptionBuilder { + return func(o *appOption) { + o.production = production + } +} + +func WithEnvironmentConfig(production, staging domain.EnvironmentConfig) AppOptionBuilder { + return func(o *appOption) { + o.production = production + o.staging = staging + } +} diff --git a/internal/deployment/fixture/app_test.go b/internal/deployment/fixture/app_test.go new file mode 100644 index 00000000..399b82b9 --- /dev/null +++ b/internal/deployment/fixture/app_test.go @@ -0,0 +1,50 @@ +package fixture_test + +import ( + "testing" + + "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" +) + +func Test_App(t *testing.T) { + t.Run("should be able to build a random app", func(t *testing.T) { + app := fixture.App() + + assert.NotZero(t, app.ID()) + }) + + t.Run("should be able to build an app with a given name", func(t *testing.T) { + app := fixture.App(fixture.WithAppName("foo")) + + created := assert.EventIs[domain.AppCreated](t, &app, 0) + assert.Equal(t, "foo", created.Name) + }) + + t.Run("should be able to build an app created by a specific user id", func(t *testing.T) { + app := fixture.App(fixture.WithAppCreatedBy("uid")) + + created := assert.EventIs[domain.AppCreated](t, &app, 0) + assert.Equal(t, "uid", created.Created.By()) + }) + + t.Run("should be able to build an app with given production and staging configuration", func(t *testing.T) { + production := domain.NewEnvironmentConfig("production_id") + staging := domain.NewEnvironmentConfig("staging_id") + app := fixture.App(fixture.WithEnvironmentConfig(production, staging)) + + created := assert.EventIs[domain.AppCreated](t, &app, 0) + assert.DeepEqual(t, production, created.Production) + assert.DeepEqual(t, staging, created.Staging) + }) + + t.Run("should be able to build an app with given production configuration", func(t *testing.T) { + config := domain.NewEnvironmentConfig("production_id") + app := fixture.App(fixture.WithProductionConfig(config)) + + created := assert.EventIs[domain.AppCreated](t, &app, 0) + assert.DeepEqual(t, config, created.Production) + assert.NotEqual(t, config.Target(), created.Staging.Target()) + }) +} diff --git a/internal/deployment/fixture/database.go b/internal/deployment/fixture/database.go new file mode 100644 index 00000000..0662fe42 --- /dev/null +++ b/internal/deployment/fixture/database.go @@ -0,0 +1,142 @@ +//go:build !release + +package fixture + +import ( + "context" + "os" + "testing" + + "github.com/YuukanOO/seelf/cmd/config" + auth "github.com/YuukanOO/seelf/internal/auth/domain" + authsqlite "github.com/YuukanOO/seelf/internal/auth/infra/sqlite" + "github.com/YuukanOO/seelf/internal/deployment/domain" + deployment "github.com/YuukanOO/seelf/internal/deployment/infra/sqlite" + "github.com/YuukanOO/seelf/pkg/bus/spy" + scheduler "github.com/YuukanOO/seelf/pkg/bus/sqlite" + "github.com/YuukanOO/seelf/pkg/log" + "github.com/YuukanOO/seelf/pkg/must" + "github.com/YuukanOO/seelf/pkg/ostools" + "github.com/YuukanOO/seelf/pkg/storage/sqlite" +) + +type ( + seed struct { + users []*auth.User + targets []*domain.Target + apps []*domain.App + deployments []*domain.Deployment + registries []*domain.Registry + } + + Context struct { + Config config.Configuration + Context context.Context // If users has been seeded, will be authenticated as the first one + Dispatcher spy.Dispatcher + TargetsStore deployment.TargetsStore + AppsStore deployment.AppsStore + DeploymentsStore deployment.DeploymentsStore + RegistriesStore deployment.RegistriesStore + } + + SeedBuilder func(*seed) +) + +func PrepareDatabase(t testing.TB, options ...SeedBuilder) *Context { + result := Context{ + Config: config.Default(config.WithTestDefaults()), + Context: context.Background(), + Dispatcher: spy.NewDispatcher(), + } + + if err := ostools.MkdirAll(result.Config.DataDir()); err != nil { + t.Fatal(err) + } + + db, err := sqlite.Open(result.Config.ConnectionString(), must.Panic(log.NewLogger()), result.Dispatcher) + + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + db.Close() + os.RemoveAll(result.Config.DataDir()) + }) + + // FIXME: scheduler migrations are needed because some migrations may queue a job by inserting inside + // the scheduled_jobs table. That's a mistake from my side and I should fix it later. + if err = db.Migrate(scheduler.Migrations, authsqlite.Migrations, deployment.Migrations); err != nil { + t.Fatal(err) + } + + result.AppsStore = deployment.NewAppsStore(db) + result.TargetsStore = deployment.NewTargetsStore(db) + result.DeploymentsStore = deployment.NewDeploymentsStore(db) + result.RegistriesStore = deployment.NewRegistriesStore(db) + + // Seed the database + var s seed + + for _, o := range options { + o(&s) + } + + if len(s.users) > 0 { + if err := authsqlite.NewUsersStore(db).Write(result.Context, s.users...); err != nil { + t.Fatal(err) + } + result.Context = auth.WithUserID(result.Context, s.users[0].ID()) // The first created user will be used as the authenticated one + } + + if err := result.RegistriesStore.Write(result.Context, s.registries...); err != nil { + t.Fatal(err) + } + + if err := result.TargetsStore.Write(result.Context, s.targets...); err != nil { + t.Fatal(err) + } + + if err := result.AppsStore.Write(result.Context, s.apps...); err != nil { + t.Fatal(err) + } + + if err := result.DeploymentsStore.Write(result.Context, s.deployments...); err != nil { + t.Fatal(err) + } + + // Reset the dispatcher after seeding + result.Dispatcher.Reset() + + return &result +} + +func WithUsers(users ...*auth.User) SeedBuilder { + return func(s *seed) { + s.users = users + } +} + +func WithTargets(targets ...*domain.Target) SeedBuilder { + return func(s *seed) { + s.targets = targets + } +} + +func WithApps(apps ...*domain.App) SeedBuilder { + return func(s *seed) { + s.apps = apps + } +} + +func WithDeployments(deployments ...*domain.Deployment) SeedBuilder { + return func(s *seed) { + s.deployments = deployments + } +} + +func WithRegistries(registries ...*domain.Registry) SeedBuilder { + return func(s *seed) { + s.registries = registries + } +} diff --git a/internal/deployment/fixture/database_test.go b/internal/deployment/fixture/database_test.go new file mode 100644 index 00000000..7879c3ee --- /dev/null +++ b/internal/deployment/fixture/database_test.go @@ -0,0 +1,53 @@ +package fixture_test + +import ( + "testing" + + auth "github.com/YuukanOO/seelf/internal/auth/domain" + authfixture "github.com/YuukanOO/seelf/internal/auth/fixture" + "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" +) + +func Test_Database(t *testing.T) { + t.Run("should be able to prepare a database without seeding it", func(t *testing.T) { + ctx := fixture.PrepareDatabase(t) + + assert.NotNil(t, ctx) + assert.NotNil(t, ctx.Config) + assert.NotNil(t, ctx.AppsStore) + assert.NotNil(t, ctx.TargetsStore) + assert.NotNil(t, ctx.AppsStore) + assert.NotNil(t, ctx.DeploymentsStore) + assert.NotNil(t, ctx.RegistriesStore) + assert.NotNil(t, ctx.Dispatcher) + assert.HasLength(t, 0, ctx.Dispatcher.Signals()) + assert.HasLength(t, 0, ctx.Dispatcher.Requests()) + }) + + t.Run("should seed correctly and attach the first user id to the created context", func(t *testing.T) { + user := authfixture.User() + target := fixture.Target(fixture.WithTargetCreatedBy(user.ID())) + config := domain.NewEnvironmentConfig(target.ID()) + app := fixture.App( + fixture.WithEnvironmentConfig(config, config), + fixture.WithAppCreatedBy(user.ID()), + ) + registry := fixture.Registry(fixture.WithRegistryCreatedBy(user.ID())) + deployment := fixture.Deployment( + fixture.FromApp(app), + fixture.WithDeploymentRequestedBy(user.ID()), + ) + + ctx := fixture.PrepareDatabase(t, + fixture.WithUsers(&user), + fixture.WithTargets(&target), + fixture.WithApps(&app), + fixture.WithRegistries(®istry), + fixture.WithDeployments(&deployment), + ) + + assert.Equal(t, user.ID(), auth.CurrentUser(ctx.Context).Get("")) + }) +} diff --git a/internal/deployment/fixture/deployment.go b/internal/deployment/fixture/deployment.go new file mode 100644 index 00000000..581f0140 --- /dev/null +++ b/internal/deployment/fixture/deployment.go @@ -0,0 +1,97 @@ +//go:build !release + +package fixture + +import ( + "database/sql/driver" + + auth "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/pkg/id" + "github.com/YuukanOO/seelf/pkg/must" + "github.com/YuukanOO/seelf/pkg/storage" +) + +type ( + deploymentOption struct { + uid auth.UserID + environment domain.Environment + source domain.SourceData + app domain.App + } + + DeploymentOptionBuilder func(*deploymentOption) +) + +func Deployment(options ...DeploymentOptionBuilder) domain.Deployment { + opts := deploymentOption{ + uid: id.New[auth.UserID](), + environment: domain.Production, + source: SourceData(), + app: App(), + } + + for _, o := range options { + o(&opts) + } + + return must.Panic(opts.app.NewDeployment(1, opts.source, opts.environment, opts.uid)) +} + +func FromApp(app domain.App) DeploymentOptionBuilder { + return func(o *deploymentOption) { + o.app = app + } +} + +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 + } +} + +func ForEnvironment(environment domain.Environment) DeploymentOptionBuilder { + return func(o *deploymentOption) { + o.environment = environment + } +} + +type ( + sourceDataOption struct { + UseVersionControl bool + } + + SourceDataOptionBuilder func(*sourceDataOption) +) + +func SourceData(options ...SourceDataOptionBuilder) domain.SourceData { + var opts sourceDataOption + + for _, o := range options { + o(&opts) + } + + return opts +} + +func (sourceDataOption) Kind() string { return "test" } +func (m sourceDataOption) NeedVersionControl() bool { return m.UseVersionControl } +func (m sourceDataOption) Value() (driver.Value, error) { return storage.ValueJSON(m) } + +func WithVersionControlNeeded() SourceDataOptionBuilder { + return func(o *sourceDataOption) { + o.UseVersionControl = true + } +} + +func init() { + domain.SourceDataTypes.Register(sourceDataOption{}, func(s string) (domain.SourceData, error) { + return storage.UnmarshalJSON[sourceDataOption](s) + }) +} diff --git a/internal/deployment/fixture/deployment_test.go b/internal/deployment/fixture/deployment_test.go new file mode 100644 index 00000000..fa0ef58e --- /dev/null +++ b/internal/deployment/fixture/deployment_test.go @@ -0,0 +1,63 @@ +package fixture_test + +import ( + "testing" + + "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" +) + +func Test_Deployment(t *testing.T) { + t.Run("should be able to create a new deployment", func(t *testing.T) { + deployment := fixture.Deployment() + + assert.NotZero(t, deployment.ID()) + assert.Equal(t, domain.Production, deployment.Config().Environment()) + }) + + t.Run("should be able to create a new deployment from a given app", func(t *testing.T) { + app := fixture.App() + deployment := fixture.Deployment(fixture.FromApp(app)) + + created := assert.EventIs[domain.DeploymentCreated](t, &deployment, 0) + assert.Equal(t, app.ID(), created.ID.AppID()) + }) + + t.Run("should be able to create a new deployment requested by a given user id", func(t *testing.T) { + deployment := fixture.Deployment(fixture.WithDeploymentRequestedBy("uid")) + + created := assert.EventIs[domain.DeploymentCreated](t, &deployment, 0) + assert.Equal(t, "uid", created.Requested.By()) + }) + + t.Run("should be able to create a new deployment with a given source data", func(t *testing.T) { + source := fixture.SourceData() + deployment := fixture.Deployment(fixture.WithSourceData(source)) + + created := assert.EventIs[domain.DeploymentCreated](t, &deployment, 0) + assert.Equal(t, source, created.Source) + }) + + t.Run("should be able to create a new deployment with a given environment", func(t *testing.T) { + deployment := fixture.Deployment(fixture.ForEnvironment(domain.Staging)) + + created := assert.EventIs[domain.DeploymentCreated](t, &deployment, 0) + assert.Equal(t, domain.Staging, created.Config.Environment()) + }) +} + +func Test_SourceData(t *testing.T) { + t.Run("should be able to create a source data", func(t *testing.T) { + source := fixture.SourceData() + + assert.Equal(t, "test", source.Kind()) + assert.False(t, source.NeedVersionControl()) + }) + + t.Run("should be able to create a source data with version control needed", func(t *testing.T) { + source := fixture.SourceData(fixture.WithVersionControlNeeded()) + + assert.True(t, source.NeedVersionControl()) + }) +} diff --git a/internal/deployment/fixture/registry.go b/internal/deployment/fixture/registry.go new file mode 100644 index 00000000..c67ce576 --- /dev/null +++ b/internal/deployment/fixture/registry.go @@ -0,0 +1,52 @@ +//go:build !release + +package fixture + +import ( + auth "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/pkg/id" + "github.com/YuukanOO/seelf/pkg/must" +) + +type ( + registryOption struct { + name string + url domain.Url + uid auth.UserID + } + + RegistryOptionBuilder func(*registryOption) +) + +func Registry(options ...RegistryOptionBuilder) domain.Registry { + opts := registryOption{ + name: id.New[string](), + url: must.Panic(domain.UrlFrom("http://" + id.New[string]() + ".com")), + uid: id.New[auth.UserID](), + } + + for _, o := range options { + o(&opts) + } + + return must.Panic(domain.NewRegistry(opts.name, domain.NewRegistryUrlRequirement(opts.url, true), opts.uid)) +} + +func WithRegistryName(name string) RegistryOptionBuilder { + return func(o *registryOption) { + o.name = name + } +} + +func WithRegistryCreatedBy(uid auth.UserID) RegistryOptionBuilder { + return func(o *registryOption) { + o.uid = uid + } +} + +func WithUrl(url domain.Url) RegistryOptionBuilder { + return func(o *registryOption) { + o.url = url + } +} diff --git a/internal/deployment/fixture/registry_test.go b/internal/deployment/fixture/registry_test.go new file mode 100644 index 00000000..2bf497c4 --- /dev/null +++ b/internal/deployment/fixture/registry_test.go @@ -0,0 +1,40 @@ +package fixture_test + +import ( + "testing" + + "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" + "github.com/YuukanOO/seelf/pkg/must" +) + +func Test_Registry(t *testing.T) { + t.Run("should be able to create a random registry", func(t *testing.T) { + registry := fixture.Registry() + + assert.NotZero(t, registry.ID()) + }) + + t.Run("should be able to create a registry with a given name", func(t *testing.T) { + registry := fixture.Registry(fixture.WithRegistryName("my-registry")) + + created := assert.EventIs[domain.RegistryCreated](t, ®istry, 0) + assert.Equal(t, "my-registry", created.Name) + }) + + t.Run("should be able to create a registry created by a given user id", func(t *testing.T) { + registry := fixture.Registry(fixture.WithRegistryCreatedBy("uid")) + + created := assert.EventIs[domain.RegistryCreated](t, ®istry, 0) + assert.Equal(t, "uid", created.Created.By()) + }) + + t.Run("should be able to create a registry with a given url", func(t *testing.T) { + url := must.Panic(domain.UrlFrom("https://my-registry.com")) + registry := fixture.Registry(fixture.WithUrl(url)) + + created := assert.EventIs[domain.RegistryCreated](t, ®istry, 0) + assert.Equal(t, url, created.Url) + }) +} diff --git a/internal/deployment/fixture/target.go b/internal/deployment/fixture/target.go new file mode 100644 index 00000000..fb3076b8 --- /dev/null +++ b/internal/deployment/fixture/target.go @@ -0,0 +1,119 @@ +//go:build !release + +package fixture + +import ( + "database/sql/driver" + + auth "github.com/YuukanOO/seelf/internal/auth/domain" + "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/pkg/id" + "github.com/YuukanOO/seelf/pkg/must" + "github.com/YuukanOO/seelf/pkg/storage" +) + +type ( + targetOption struct { + name string + provider domain.ProviderConfig + uid auth.UserID + } + + TargetOptionBuilder func(*targetOption) +) + +func Target(options ...TargetOptionBuilder) domain.Target { + opts := targetOption{ + name: id.New[string](), + provider: ProviderConfig(), + uid: id.New[auth.UserID](), + } + + for _, o := range options { + o(&opts) + } + + return must.Panic(domain.NewTarget(opts.name, + domain.NewProviderConfigRequirement(opts.provider, true), + opts.uid)) +} + +func WithTargetName(name string) TargetOptionBuilder { + return func(opts *targetOption) { + opts.name = name + } +} + +func WithTargetCreatedBy(uid auth.UserID) TargetOptionBuilder { + return func(opts *targetOption) { + opts.uid = uid + } +} + +func WithProviderConfig(config domain.ProviderConfig) TargetOptionBuilder { + return func(opts *targetOption) { + opts.provider = config + } +} + +type ( + providerConfig struct { + Kind_ string + Data string + Fingerprint_ string + } + + ProviderConfigBuilder func(*providerConfig) +) + +func ProviderConfig(options ...ProviderConfigBuilder) (result domain.ProviderConfig) { + config := providerConfig{ + Data: id.New[string](), + Kind_: id.New[string](), + Fingerprint_: id.New[string](), + } + + for _, o := range options { + o(&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 { + return func(config *providerConfig) { + config.Fingerprint_ = fingerprint + } +} + +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) } + +func (d providerConfig) Equals(other domain.ProviderConfig) bool { + return d == other +} diff --git a/internal/deployment/fixture/target_test.go b/internal/deployment/fixture/target_test.go new file mode 100644 index 00000000..32ddba7c --- /dev/null +++ b/internal/deployment/fixture/target_test.go @@ -0,0 +1,76 @@ +package fixture_test + +import ( + "testing" + + "github.com/YuukanOO/seelf/internal/deployment/domain" + "github.com/YuukanOO/seelf/internal/deployment/fixture" + "github.com/YuukanOO/seelf/pkg/assert" +) + +func Test_Target(t *testing.T) { + t.Run("should be able to create a target", func(t *testing.T) { + target := fixture.Target() + + assert.NotZero(t, target.ID()) + }) + + t.Run("should be able to create a target with a given name", func(t *testing.T) { + target := fixture.Target(fixture.WithTargetName("name")) + + created := assert.EventIs[domain.TargetCreated](t, &target, 0) + assert.Equal(t, "name", created.Name) + }) + + t.Run("should be able to create a target with a given user id", func(t *testing.T) { + target := fixture.Target(fixture.WithTargetCreatedBy("id")) + + created := assert.EventIs[domain.TargetCreated](t, &target, 0) + assert.Equal(t, "id", created.Created.By()) + }) + + t.Run("should be able to create a target with a given provider config", func(t *testing.T) { + config := fixture.ProviderConfig() + target := fixture.Target(fixture.WithProviderConfig(config)) + + created := assert.EventIs[domain.TargetCreated](t, &target, 0) + assert.DeepEqual(t, config, created.Provider) + }) +} + +func Test_ProviderConfig(t *testing.T) { + t.Run("should be able to create a provider config", func(t *testing.T) { + config := fixture.ProviderConfig() + + assert.NotZero(t, config.Fingerprint()) + assert.NotZero(t, config.Kind()) + }) + + t.Run("should be able to create a provider config with a given fingerprint", func(t *testing.T) { + config := fixture.ProviderConfig(fixture.WithFingerprint("fingerprint")) + + assert.Equal(t, "fingerprint", config.Fingerprint()) + }) + + t.Run("should be able to create a provider config with a given kind", func(t *testing.T) { + config := fixture.ProviderConfig(fixture.WithKind("kind")) + + assert.Equal(t, "kind", config.Kind()) + }) + + t.Run("should be able to create a provider config with a given data", func(t *testing.T) { + one := fixture.ProviderConfig( + fixture.WithKind("kind"), + fixture.WithFingerprint("fingerprint"), + fixture.WithData("data")) + two := fixture.ProviderConfig( + fixture.WithKind("kind"), + fixture.WithFingerprint("fingerprint"), + fixture.WithData("data")) + three := fixture.ProviderConfig() + + assert.True(t, one.Equals(two)) + assert.False(t, one.Equals(three)) + assert.False(t, two.Equals(three)) + }) +} diff --git a/internal/deployment/infra/artifact/local_artifact_manager.go b/internal/deployment/infra/artifact/local_artifact_manager.go index fa88c12b..a5711d35 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 @@ -121,15 +121,15 @@ func (a *localArtifactManager) appPath(appID domain.AppID) string { return filepath.Join(a.appsDirectory, string(appID)) } -func (a *localArtifactManager) deploymentPath(depl domain.Deployment) (string, error) { +func (a *localArtifactManager) deploymentPath(deployment domain.Deployment) (string, error) { var w strings.Builder if err := a.options.DeploymentDirTemplate().Execute(&w, deploymentTemplateData{ - Number: depl.ID().DeploymentNumber(), - Environment: depl.Config().Environment(), + Number: deployment.ID().DeploymentNumber(), + Environment: deployment.Config().Environment(), }); err != nil { return "", err } - return filepath.Join(a.appPath(depl.ID().AppID()), w.String()), nil + return filepath.Join(a.appPath(deployment.ID().AppID()), w.String()), nil } diff --git a/internal/deployment/infra/artifact/local_artifact_manager_test.go b/internal/deployment/infra/artifact/local_artifact_manager_test.go index df9c1137..ec9bb00a 100644 --- a/internal/deployment/infra/artifact/local_artifact_manager_test.go +++ b/internal/deployment/infra/artifact/local_artifact_manager_test.go @@ -9,9 +9,9 @@ import ( "github.com/YuukanOO/seelf/internal/deployment/domain" "github.com/YuukanOO/seelf/internal/deployment/infra/artifact" "github.com/YuukanOO/seelf/internal/deployment/infra/source/raw" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/log" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_LocalArtifactManager(t *testing.T) { @@ -34,27 +34,27 @@ func Test_LocalArtifactManager(t *testing.T) { manager := sut() ctx, err := manager.PrepareBuild(context.Background(), depl) - testutil.IsNil(t, err) - testutil.IsNotNil(t, logger) + assert.Nil(t, err) + assert.NotNil(t, logger) defer ctx.Logger().Close() _, err = os.ReadDir(ctx.BuildDirectory()) - testutil.IsNil(t, err) + assert.Nil(t, err) }) t.Run("should correctly cleanup an app directory", func(t *testing.T) { manager := sut() ctx, err := manager.PrepareBuild(context.Background(), depl) - testutil.IsNil(t, err) + assert.Nil(t, err) ctx.Logger().Close() // Do not defer or else the directory will be locked err = manager.Cleanup(context.Background(), app.ID()) - testutil.IsNil(t, err) + assert.Nil(t, err) _, err = os.ReadDir(ctx.BuildDirectory()) - testutil.IsTrue(t, os.IsNotExist(err)) + assert.True(t, os.IsNotExist(err)) }) } diff --git a/internal/deployment/infra/artifact/logger.go b/internal/deployment/infra/artifact/logger.go index 8ea5edf2..6e8cd972 100644 --- a/internal/deployment/infra/artifact/logger.go +++ b/internal/deployment/infra/artifact/logger.go @@ -41,5 +41,5 @@ func (l *stepLogger) Close() error { } func (l *stepLogger) print(prefix string, format string, args []any) { - l.Write([]byte(prefix + " " + fmt.Sprintf(format, args...) + "\n")) + _, _ = l.Write([]byte(prefix + " " + fmt.Sprintf(format, args...) + "\n")) } diff --git a/internal/deployment/infra/memory/apps.go b/internal/deployment/infra/memory/apps.go deleted file mode 100644 index 77606b18..00000000 --- a/internal/deployment/infra/memory/apps.go +++ /dev/null @@ -1,203 +0,0 @@ -package memory - -import ( - "context" - - "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/apperr" - "github.com/YuukanOO/seelf/pkg/event" - "github.com/YuukanOO/seelf/pkg/monad" -) - -type ( - AppsStore interface { - domain.AppsReader - domain.AppsWriter - } - - appsStore struct { - apps []*appData - } - - appData struct { - id domain.AppID - name domain.AppName - productionTarget domain.TargetID - stagingTarget domain.TargetID - value *domain.App - } -) - -func NewAppsStore(existingApps ...*domain.App) AppsStore { - s := &appsStore{} - - s.Write(context.Background(), existingApps...) - - return s -} - -func (s *appsStore) CheckAppNamingAvailability( - ctx context.Context, - name domain.AppName, - production domain.EnvironmentConfig, - staging domain.EnvironmentConfig, -) (domain.EnvironmentConfigRequirement, domain.EnvironmentConfigRequirement, error) { - var productionTaken, stagingTaken bool - - for _, app := range s.apps { - if app.name != name { - continue - } - - if app.productionTarget == production.Target() { - productionTaken = true - } - - if app.stagingTarget == staging.Target() { - stagingTaken = true - } - } - - return domain.NewEnvironmentConfigRequirement(production, true, !productionTaken), - domain.NewEnvironmentConfigRequirement(staging, true, !stagingTaken), - nil -} - -func (s *appsStore) CheckAppNamingAvailabilityByID( - ctx context.Context, - id domain.AppID, - production monad.Maybe[domain.EnvironmentConfig], - staging monad.Maybe[domain.EnvironmentConfig], -) ( - productionRequirement domain.EnvironmentConfigRequirement, - stagingRequirement domain.EnvironmentConfigRequirement, - err error, -) { - productionValue, hasProductionTarget := production.TryGet() - stagingValue, hasStagingTarget := staging.TryGet() - - // No input, no check! - if !hasProductionTarget && !hasStagingTarget { - return productionRequirement, stagingRequirement, nil - } - - // Retrieve app name by its ID - var name domain.AppName - - for _, app := range s.apps { - if app.id == id { - name = app.name - break - } - } - - if name == "" { - return productionRequirement, stagingRequirement, apperr.ErrNotFound - } - - var productionTaken, stagingTaken bool - - // And check if an app on the target and env already exists - for _, app := range s.apps { - if app.id == id || app.name != name { - continue - } - - if hasProductionTarget && app.productionTarget == productionValue.Target() { - productionTaken = true - } - - if hasStagingTarget && app.stagingTarget == stagingValue.Target() { - stagingTaken = true - } - } - - if hasProductionTarget { - productionRequirement = domain.NewEnvironmentConfigRequirement(productionValue, true, !productionTaken) - } - - if hasStagingTarget { - stagingRequirement = domain.NewEnvironmentConfigRequirement(stagingValue, true, !stagingTaken) - } - - return productionRequirement, stagingRequirement, nil -} - -func (s *appsStore) HasAppsOnTarget(ctx context.Context, target domain.TargetID) (domain.HasAppsOnTarget, error) { - for _, app := range s.apps { - if app.productionTarget == target || app.stagingTarget == target { - return true, nil - } - } - - return false, nil -} - -func (s *appsStore) GetByID(ctx context.Context, id domain.AppID) (domain.App, error) { - for _, app := range s.apps { - if app.id == id { - return *app.value, nil - } - } - - return domain.App{}, apperr.ErrNotFound -} - -func (s *appsStore) Write(ctx context.Context, apps ...*domain.App) error { - for _, app := range apps { - for _, e := range event.Unwrap(app) { - switch evt := e.(type) { - case domain.AppCreated: - var exist bool - for _, a := range s.apps { - if a.id == evt.ID { - exist = true - break - } - } - - if exist { - continue - } - - s.apps = append(s.apps, &appData{ - id: evt.ID, - name: evt.Name, - productionTarget: evt.Production.Target(), - stagingTarget: evt.Staging.Target(), - value: app, - }) - case domain.AppEnvChanged: - for _, a := range s.apps { - if a.id == app.ID() { - switch evt.Environment { - case domain.Production: - a.productionTarget = evt.Config.Target() - case domain.Staging: - a.stagingTarget = evt.Config.Target() - } - *a.value = *app - break - } - } - case domain.AppDeleted: - for i, a := range s.apps { - if a.id == app.ID() { - *a.value = *app - s.apps = append(s.apps[:i], s.apps[i+1:]...) - break - } - } - default: - for _, a := range s.apps { - if a.id == app.ID() { - *a.value = *app - break - } - } - } - } - } - - return nil -} diff --git a/internal/deployment/infra/memory/deployments.go b/internal/deployment/infra/memory/deployments.go deleted file mode 100644 index 9e1c3ff6..00000000 --- a/internal/deployment/infra/memory/deployments.go +++ /dev/null @@ -1,162 +0,0 @@ -package memory - -import ( - "context" - - "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/apperr" - shared "github.com/YuukanOO/seelf/pkg/domain" - "github.com/YuukanOO/seelf/pkg/event" -) - -type ( - DeploymentsStore interface { - domain.DeploymentsReader - domain.DeploymentsWriter - } - - deploymentsStore struct { - deployments []*deploymentData - } - - deploymentData struct { - id domain.DeploymentID - value *domain.Deployment - state domain.DeploymentState - } -) - -func NewDeploymentsStore(existingDeployments ...*domain.Deployment) DeploymentsStore { - s := &deploymentsStore{} - - s.Write(context.Background(), existingDeployments...) - - return s -} - -func (s *deploymentsStore) GetByID(ctx context.Context, id domain.DeploymentID) (domain.Deployment, error) { - for _, depl := range s.deployments { - if depl.id == id { - return *depl.value, nil - } - } - - return domain.Deployment{}, apperr.ErrNotFound -} - -func (s *deploymentsStore) GetLastDeployment(ctx context.Context, id domain.AppID, env domain.Environment) (domain.Deployment, error) { - var last *deploymentData - - for _, depl := range s.deployments { - if depl.id.AppID() == id && depl.value.Config().Environment() == env { - if last == nil || last.id.DeploymentNumber() < depl.id.DeploymentNumber() { - last = depl - } - } - } - - if last == nil { - return domain.Deployment{}, apperr.ErrNotFound - } - - return *last.value, nil - -} - -func (s *deploymentsStore) GetNextDeploymentNumber(ctx context.Context, appid domain.AppID) (domain.DeploymentNumber, error) { - count := 0 - - for _, depl := range s.deployments { - if depl.id.AppID() == appid { - count += 1 - } - } - - return domain.DeploymentNumber(count + 1), nil -} - -func (s *deploymentsStore) HasRunningOrPendingDeploymentsOnTarget(ctx context.Context, target domain.TargetID) (domain.HasRunningOrPendingDeploymentsOnTarget, error) { - for _, d := range s.deployments { - if d.value.Config().Target() == target && (d.state.Status() == domain.DeploymentStatusRunning || d.state.Status() == domain.DeploymentStatusPending) { - return true, nil - } - } - - return false, nil -} - -func (s *deploymentsStore) HasDeploymentsOnAppTargetEnv(ctx context.Context, app domain.AppID, target domain.TargetID, env domain.Environment, ti shared.TimeInterval) ( - domain.HasRunningOrPendingDeploymentsOnAppTargetEnv, - domain.HasSuccessfulDeploymentsOnAppTargetEnv, - error, -) { - var ( - ongoing domain.HasRunningOrPendingDeploymentsOnAppTargetEnv - successful domain.HasSuccessfulDeploymentsOnAppTargetEnv - ) - - for _, d := range s.deployments { - if d.id.AppID() != app || d.value.Config().Target() != target || d.value.Config().Environment() != env { - continue - } - - switch d.state.Status() { - case domain.DeploymentStatusSucceeded: - if d.value.Requested().At().After(ti.From()) && d.value.Requested().At().Before(ti.To()) { - successful = true - } - case domain.DeploymentStatusRunning, domain.DeploymentStatusPending: - ongoing = true - } - } - - return ongoing, successful, nil -} - -func (s *deploymentsStore) FailDeployments(ctx context.Context, reason error, criterias domain.FailCriterias) error { - panic("not implemented") -} - -func (s *deploymentsStore) Write(ctx context.Context, deployments ...*domain.Deployment) error { - for _, depl := range deployments { - for _, e := range event.Unwrap(depl) { - switch evt := e.(type) { - case domain.DeploymentCreated: - var exist bool - for _, a := range s.deployments { - if a.id == evt.ID { - exist = true - break - } - } - - if exist { - continue - } - - s.deployments = append(s.deployments, &deploymentData{ - id: evt.ID, - value: depl, - state: evt.State, - }) - case domain.DeploymentStateChanged: - for _, d := range s.deployments { - if d.id == depl.ID() { - *d.value = *depl - d.state = evt.State - break - } - } - default: - for _, d := range s.deployments { - if d.id == depl.ID() { - *d.value = *depl - break - } - } - } - } - } - - return nil -} diff --git a/internal/deployment/infra/memory/registries.go b/internal/deployment/infra/memory/registries.go deleted file mode 100644 index 6889751b..00000000 --- a/internal/deployment/infra/memory/registries.go +++ /dev/null @@ -1,110 +0,0 @@ -package memory - -import ( - "context" - "slices" - - "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/apperr" - "github.com/YuukanOO/seelf/pkg/event" -) - -type ( - RegistriesStore interface { - domain.RegistriesReader - domain.RegistriesWriter - } - - registriesStore struct { - registries []*registryData - } - - registryData struct { - id domain.RegistryID - value *domain.Registry - } -) - -func NewRegistriesStore(existingApps ...*domain.Registry) RegistriesStore { - s := ®istriesStore{} - - s.Write(context.Background(), existingApps...) - - return s -} - -func (s *registriesStore) CheckUrlAvailability(ctx context.Context, domainUrl domain.Url, excluded ...domain.RegistryID) (domain.RegistryUrlRequirement, error) { - var registry *domain.Registry - - for _, t := range s.registries { - if t.value.Url() == domainUrl { - registry = t.value - break - } - } - - return domain.NewRegistryUrlRequirement(domainUrl, registry == nil || slices.Contains(excluded, registry.ID())), nil -} - -func (s *registriesStore) GetByID(ctx context.Context, id domain.RegistryID) (domain.Registry, error) { - for _, r := range s.registries { - if r.id == id { - return *r.value, nil - } - } - - return domain.Registry{}, apperr.ErrNotFound -} - -func (s *registriesStore) GetAll(ctx context.Context) ([]domain.Registry, error) { - var registries []domain.Registry - - for _, r := range s.registries { - registries = append(registries, *r.value) - } - - return registries, nil -} - -func (s *registriesStore) Write(ctx context.Context, registries ...*domain.Registry) error { - for _, reg := range registries { - for _, e := range event.Unwrap(reg) { - switch evt := e.(type) { - case domain.RegistryCreated: - var exist bool - for _, r := range s.registries { - if r.id == evt.ID { - exist = true - break - } - } - - if exist { - continue - } - - s.registries = append(s.registries, ®istryData{ - id: evt.ID, - value: reg, - }) - case domain.RegistryDeleted: - for i, r := range s.registries { - if r.id == reg.ID() { - *r.value = *reg - s.registries = append(s.registries[:i], s.registries[i+1:]...) - break - } - } - default: - for _, r := range s.registries { - if r.id == reg.ID() { - *r.value = *reg - break - } - } - } - } - } - - return nil -} diff --git a/internal/deployment/infra/memory/targets.go b/internal/deployment/infra/memory/targets.go deleted file mode 100644 index 75fde1a7..00000000 --- a/internal/deployment/infra/memory/targets.go +++ /dev/null @@ -1,133 +0,0 @@ -package memory - -import ( - "context" - "slices" - - "github.com/YuukanOO/seelf/internal/deployment/domain" - "github.com/YuukanOO/seelf/pkg/apperr" - "github.com/YuukanOO/seelf/pkg/event" -) - -type ( - TargetsStore interface { - domain.TargetsReader - domain.TargetsWriter - } - - targetsStore struct { - targets []*targetData - } - - targetData struct { - id domain.TargetID - domain domain.Url - value *domain.Target - } -) - -func NewTargetsStore(existingTargets ...*domain.Target) TargetsStore { - s := &targetsStore{} - - s.Write(context.Background(), existingTargets...) - - return s -} - -func (s *targetsStore) CheckUrlAvailability(ctx context.Context, domainUrl domain.Url, excluded ...domain.TargetID) (domain.TargetUrlRequirement, error) { - var target *domain.Target - - for _, t := range s.targets { - if t.domain.String() == domainUrl.String() { - target = t.value - break - } - } - - return domain.NewTargetUrlRequirement(domainUrl, target == nil || slices.Contains(excluded, target.ID())), nil -} - -func (s *targetsStore) CheckConfigAvailability(ctx context.Context, config domain.ProviderConfig, excluded ...domain.TargetID) (domain.ProviderConfigRequirement, error) { - var target *domain.Target - - for _, t := range s.targets { - if t.value.Provider().Fingerprint() == config.Fingerprint() { - target = t.value - break - } - } - - return domain.NewProviderConfigRequirement(config, target == nil || slices.Contains(excluded, target.ID())), nil -} - -func (s *targetsStore) GetLocalTarget(ctx context.Context) (domain.Target, error) { - for _, t := range s.targets { - if t.value.Provider().Fingerprint() == "" { - return *t.value, nil - } - } - - return domain.Target{}, apperr.ErrNotFound -} - -func (s *targetsStore) GetByID(ctx context.Context, id domain.TargetID) (domain.Target, error) { - for _, t := range s.targets { - if t.id == id { - return *t.value, nil - } - } - - return domain.Target{}, apperr.ErrNotFound -} - -func (s *targetsStore) Write(ctx context.Context, targets ...*domain.Target) error { - for _, target := range targets { - for _, e := range event.Unwrap(target) { - switch evt := e.(type) { - case domain.TargetCreated: - var exist bool - for _, a := range s.targets { - if a.id == evt.ID { - exist = true - break - } - } - - if exist { - continue - } - - s.targets = append(s.targets, &targetData{ - id: evt.ID, - domain: evt.Url, - value: target, - }) - case domain.TargetUrlChanged: - for _, t := range s.targets { - if t.id == evt.ID { - t.domain = evt.Url - *t.value = *target - break - } - } - case domain.TargetDeleted: - for i, t := range s.targets { - if t.id == target.ID() { - *t.value = *target - s.targets = append(s.targets[:i], s.targets[i+1:]...) - break - } - } - default: - for _, t := range s.targets { - if t.id == target.ID() { - *t.value = *target - break - } - } - } - } - } - - return nil -} diff --git a/internal/deployment/infra/mod.go b/internal/deployment/infra/mod.go index ee0db6f4..568ec385 100644 --- a/internal/deployment/infra/mod.go +++ b/internal/deployment/infra/mod.go @@ -124,7 +124,7 @@ func Setup( } // Fail running deployments in case of a hard reset. - return deploymentsStore.FailDeployments(context.Background(), errors.New("server_reset"), domain.FailCriterias{ + return deploymentsStore.FailDeployments(context.Background(), errors.New("server_reset"), domain.FailCriteria{ Status: monad.Value(domain.DeploymentStatusRunning), }) } 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/data_test.go b/internal/deployment/infra/provider/docker/data_test.go index 16a0e004..d7294f10 100644 --- a/internal/deployment/infra/provider/docker/data_test.go +++ b/internal/deployment/infra/provider/docker/data_test.go @@ -6,9 +6,9 @@ import ( "github.com/YuukanOO/seelf/internal/deployment/domain" "github.com/YuukanOO/seelf/internal/deployment/infra/provider/docker" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/ssh" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_Data(t *testing.T) { @@ -56,7 +56,7 @@ func Test_Data(t *testing.T) { t.Run(fmt.Sprintf("%v", test), func(t *testing.T) { got := test.a.Equals(test.b) - testutil.Equals(t, test.expected, got) + assert.Equal(t, test.expected, got) }) } }) diff --git a/internal/deployment/infra/provider/docker/deployment.go b/internal/deployment/infra/provider/docker/deployment.go index 09f7c07e..a0e07d22 100644 --- a/internal/deployment/infra/provider/docker/deployment.go +++ b/internal/deployment/infra/provider/docker/deployment.go @@ -18,31 +18,41 @@ 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) + } -func newDeploymentProjectBuilder(ctx domain.DeploymentContext, depl domain.Deployment) *deploymentProjectBuilder { - config := depl.Config() + deploymentProjectBuilder struct { + exposedManually bool + sourceDir string + composePath string + networkName string + config domain.ConfigSnapshot + logger domain.DeploymentLogger + labels types.Labels + routersByPort map[string]domain.Router + services domain.Services + project *types.Project + } +) + +func newDeploymentProjectBuilder( + ctx domain.DeploymentContext, + deployment domain.Deployment, + target domain.Target, +) DeploymentProjectBuilder { + config := deployment.Config() return &deploymentProjectBuilder{ - isDefaultSubdomainAvailable: true, - sourceDir: ctx.BuildDirectory(), - config: config, - networkName: targetPublicNetworkName(config.Target()), - logger: ctx.Logger(), - routersByPort: make(map[string]domain.Router), + exposedManually: target.IsManual(), + sourceDir: ctx.BuildDirectory(), + config: config, + networkName: targetPublicNetworkName(config.Target()), + 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 +121,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 +136,10 @@ func (b *deploymentProjectBuilder) loadProject(ctx context.Context) error { }, }, } - }), - ) + })) + } + + opts, err := cli.NewProjectOptions([]string{b.composePath}, loaders...) if err != nil { b.logger.Error(err) @@ -154,11 +169,13 @@ func (b *deploymentProjectBuilder) transform() { b.project.DisabledServices = nil // Reset the list of disabled services or orphans created for an old profile will not be deleted } + builder := b.config.ServicesBuilder() + // 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) + service := builder.AddService(serviceDefinition.Name, serviceDefinition.Image) serviceName := service.Name() if serviceDefinition.Restart == "" { @@ -195,10 +212,9 @@ 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 } @@ -215,19 +231,18 @@ func (b *deploymentProjectBuilder) transform() { continue } + port := domain.Port(portConfig.Target) + switch router { case domain.RouterHttp: - entrypoint = service.AddHttpEntrypoint(b.config, domain.Port(portConfig.Target), domain.HttpEntrypointOptions{ - Managed: httpMainEntryPointAvailable, - UseDefaultSubdomain: b.isDefaultSubdomainAvailable, - }) + entrypoint = service.AddHttpEntrypoint(port, !httpMainEntryPointAvailable) httpMainEntryPointAvailable = false - serviceDefinition.Labels[SubdomainLabel] = entrypoint.Subdomain().MustGet() + serviceDefinition.Labels[SubdomainLabel] = entrypoint.Subdomain().Get("") case domain.RouterTcp: - entrypoint = service.AddTCPEntrypoint(domain.Port(portConfig.Target)) + entrypoint = service.AddTCPEntrypoint(port, true) serviceDefinition.Labels["traefik.tcp.routers."+string(entrypoint.Name())+".rule"] = "HostSNI(`*`)" case domain.RouterUdp: - entrypoint = service.AddUDPEntrypoint(domain.Port(portConfig.Target)) + entrypoint = service.AddUDPEntrypoint(port, true) default: b.logger.Warnf("unsupported router type for service %s, the service will not be exposed", serviceName) continue @@ -240,7 +255,6 @@ func (b *deploymentProjectBuilder) transform() { if !entrypoint.IsCustom() { serviceDefinition.Labels["traefik."+routerName+".routers."+entrypointName+".entrypoints"] = httpMainEntryPoint - b.isDefaultSubdomainAvailable = false } else { serviceDefinition.Labels[CustomEntrypointsLabel] = "true" serviceDefinition.Labels["traefik."+routerName+".routers."+entrypointName+".entrypoints"] = entrypointName @@ -261,9 +275,10 @@ func (b *deploymentProjectBuilder) transform() { // Update the project definition and state b.project.Services[serviceName] = serviceDefinition - b.services = append(b.services, service) } + b.services = builder.Services() + // Add labels to network and volumes to make it easy to find them for name, network := range b.project.Networks { network.Labels = appendLabels(network.Labels, b.labels) @@ -276,6 +291,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 5febbdee..e632f88f 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,19 +13,21 @@ 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" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/log" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/must" "github.com/YuukanOO/seelf/pkg/ssh" - "github.com/YuukanOO/seelf/pkg/testutil" "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" "github.com/docker/compose/v2/pkg/api" dockertypes "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" ) @@ -36,14 +39,14 @@ type options interface { func Test_Provider(t *testing.T) { logger := must.Panic(log.NewLogger()) - sut := func(opts options) (docker.Docker, *dockerMockService) { + arrange := func(opts options) (docker.Docker, *dockerMockService) { mock := newMockService() t.Cleanup(func() { 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) { @@ -200,310 +203,582 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID }, } - provider, _ := sut(config.Default(config.WithTestDefaults())) + provider, _ := arrange(config.Default(config.WithTestDefaults())) for _, tt := range tests { t.Run(fmt.Sprintf("%v", tt.payload), func(t *testing.T) { data, err := provider.Prepare(context.Background(), tt.payload, tt.existing...) - testutil.IsNil(t, err) - testutil.IsTrue(t, data.Equals(tt.expected)) + assert.Nil(t, err) + assert.True(t, data.Equals(tt.expected)) }) } }) - 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 := sut(config.Default(config.WithTestDefaults())) - - assigned, err := provider.Setup(context.Background(), target) - - testutil.IsNil(t, err) - testutil.DeepEquals(t, domain.TargetEntrypointsAssigned{}, assigned) - testutil.HasLength(t, mock.ups, 1) - testutil.DeepEquals(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 := sut(config.Default(config.WithTestDefaults())) - - assigned, err := provider.Setup(context.Background(), target) + }, mock.ups[0].project) + }) - testutil.IsNil(t, err) - testutil.DeepEquals(t, domain.TargetEntrypointsAssigned{}, assigned) - testutil.HasLength(t, mock.ups, 1) - testutil.DeepEquals(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 := sut(config.Default(config.WithTestDefaults())) - - assigned, err := provider.Setup(context.Background(), target) - - testutil.IsNil(t, err) - testutil.HasLength(t, mock.ups, 2) - testutil.HasLength(t, mock.downs, 1) - - tcpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][tcp.Name()] - udpPort := assigned[depl.ID().AppID()][depl.Config().Environment()][udp.Name()] - - testutil.NotEquals(t, 0, tcpPort) - testutil.NotEquals(t, 0, udpPort) + }, mock.ups[0].project) + }) - testutil.DeepEquals(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)) + builder := deployment.Config().ServicesBuilder() + service := builder.AddService("app", "") + tcp := service.AddTCPEntrypoint(5432, true) + udp := service.AddUDPEntrypoint(5433, true) + target.ExposeEntrypoints(deployment.Config().AppID(), deployment.Config().Environment(), builder.Services()) + 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)) + builder := deployment.Config().ServicesBuilder() + service := builder.AddService("app", "") + tcp := service.AddTCPEntrypoint(5432, true) + udp := service.AddUDPEntrypoint(5433, true) + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), builder.Services()) + 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, true) + newUdp := service.AddUDPEntrypoint(5435, true) + target.ExposeEntrypoints(deployment.ID().AppID(), deployment.Config().Environment(), builder.Services()) + 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 := sut(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) - testutil.IsNil(t, err) - testutil.HasLength(t, mock.ups, 2) - testutil.HasLength(t, mock.downs, 1) - testutil.Equals(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) + }) - testutil.NotEquals(t, 0, tcpPort) - testutil.NotEquals(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) + } + } - testutil.DeepEquals(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) - }) + }, 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: @@ -536,196 +811,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) - testutil.IsNil(t, err) - testutil.IsNil(t, raw.New().Fetch(context.Background(), ctx, depl)) - - provider, mock := sut(opts) - - services, err := provider.Deploy(context.Background(), ctx, depl, target, nil) - - testutil.IsNil(t, err) - testutil.HasLength(t, mock.ups, 1) - testutil.HasLength(t, services, 3) - - testutil.Equals(t, "app", services[0].Name()) - testutil.Equals(t, "db", services[1].Name()) - testutil.Equals(t, "sidecar", services[2].Name()) - - entrypoints := services.Entrypoints() - testutil.HasLength(t, entrypoints, 4) - testutil.Equals(t, 8080, entrypoints[0].Port()) - testutil.Equals(t, "http", entrypoints[0].Router()) - testutil.Equals(t, string(depl.Config().AppName()), entrypoints[0].Subdomain().Get("")) - testutil.Equals(t, 8081, entrypoints[1].Port()) - testutil.Equals(t, "udp", entrypoints[1].Router()) - testutil.Equals(t, 8082, entrypoints[2].Port()) - testutil.Equals(t, "http", entrypoints[2].Router()) - testutil.Equals(t, string(depl.Config().AppName()), entrypoints[2].Subdomain().Get("")) - testutil.Equals(t, 5432, entrypoints[3].Port()) - testutil.Equals(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())) - testutil.Equals(t, expectedProjectName, project.Name) - testutil.Equals(t, 3, len(project.Services)) - - for _, service := range project.Services { - switch service.Name { - case "sidecar": - testutil.Equals(t, "traefik/whoami", service.Image) - testutil.HasLength(t, service.Ports, 0) - testutil.DeepEquals(t, types.MappingWithEquals{}, service.Environment) - testutil.DeepEquals(t, types.Labels{ - docker.AppLabel: string(depl.ID().AppID()), - docker.TargetLabel: string(target.ID()), - docker.EnvironmentLabel: string(depl.Config().Environment()), - }, service.Labels) - testutil.DeepEquals(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"] - - testutil.Equals(t, fmt.Sprintf("%s-%s/app:%s", depl.Config().AppName(), appIdLower, depl.Config().Environment()), service.Image) - testutil.Equals(t, types.RestartPolicyUnlessStopped, service.Restart) - testutil.DeepEquals(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) - - testutil.HasLength(t, service.Ports, 0) - testutil.DeepEquals(t, types.MappingWithEquals{ - "DSN": &dsn, - }, service.Environment) - testutil.DeepEquals(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"] - - testutil.Equals(t, "postgres:14-alpine", service.Image) - testutil.Equals(t, types.RestartPolicyUnlessStopped, service.Restart) - testutil.DeepEquals(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) - testutil.HasLength(t, service.Ports, 0) - testutil.DeepEquals(t, types.MappingWithEquals{ - "POSTGRES_USER": &postgresUser, - "POSTGRES_PASSWORD": &postgresPassword, - }, service.Environment) - testutil.DeepEquals(t, map[string]*types.ServiceNetworkConfig{ - "default": nil, - expectedGatewayNetworkName: nil, - }, service.Networks) - testutil.DeepEquals(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) + } } - } - testutil.DeepEquals(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) - testutil.DeepEquals(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) - - testutil.DeepEquals(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 { @@ -822,23 +1057,7 @@ func (d *dockerMockCli) ContainerInspect(_ context.Context, containerName string return result, nil } -func (d *dockerMockCli) ImagesPrune(_ context.Context, criteria filters.Args) (dockertypes.ImagesPruneReport, error) { +func (d *dockerMockCli) ImagesPrune(_ context.Context, criteria filters.Args) (image.PruneReport, error) { d.parent.pruneFilters = criteria - return dockertypes.ImagesPruneReport{}, nil + 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 83eb670e..014bc92e 100644 --- a/internal/deployment/infra/provider/facade_test.go +++ b/internal/deployment/infra/provider/facade_test.go @@ -5,78 +5,64 @@ 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/must" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" ) 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() _, err := sut.Prepare(context.Background(), "payload") - testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err) + assert.ErrorIs(t, domain.ErrNoValidProviderFound, err) }) 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) - testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err) + assert.ErrorIs(t, domain.ErrNoValidProviderFound, err) }) 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) - testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err) + assert.ErrorIs(t, domain.ErrNoValidProviderFound, err) }) 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) - testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err) + assert.ErrorIs(t, domain.ErrNoValidProviderFound, err) }) 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) - testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err) + assert.ErrorIs(t, domain.ErrNoValidProviderFound, err) }) 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) - testutil.ErrorIs(t, domain.ErrNoValidProviderFound, err) + 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/source/git/data.go b/internal/deployment/infra/source/git/data.go index 6f4a1e77..97aa7f00 100644 --- a/internal/deployment/infra/source/git/data.go +++ b/internal/deployment/infra/source/git/data.go @@ -25,7 +25,7 @@ func init() { }) // Here the registered discriminated type is the same since there are no unexposed fields and - // it also handle the retrocompatibility with the old payload format. + // it also handle the retro-compatibility with the old payload format. get_deployment.SourceDataTypes.Register(Data{}, func(s string) (get_deployment.SourceData, error) { return tryParseGitData(s) }) diff --git a/internal/deployment/infra/sqlite/deployments.go b/internal/deployment/infra/sqlite/deployments.go index 67a098b0..db61b94d 100644 --- a/internal/deployment/infra/sqlite/deployments.go +++ b/internal/deployment/infra/sqlite/deployments.go @@ -131,7 +131,7 @@ func (s *deploymentsStore) HasDeploymentsOnAppTargetEnv(ctx context.Context, app domain.HasSuccessfulDeploymentsOnAppTargetEnv(c.successful), err } -func (s *deploymentsStore) FailDeployments(ctx context.Context, reason error, criterias domain.FailCriterias) error { +func (s *deploymentsStore) FailDeployments(ctx context.Context, reason error, criterias domain.FailCriteria) error { now := time.Now().UTC() return builder.Update("deployments", builder.Values{ 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/apperr/error_test.go b/pkg/apperr/error_test.go index 3ae07423..2cf815de 100644 --- a/pkg/apperr/error_test.go +++ b/pkg/apperr/error_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/YuukanOO/seelf/pkg/apperr" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" ) func Test_Error(t *testing.T) { @@ -13,37 +13,37 @@ func Test_Error(t *testing.T) { msg := "an error !" err := apperr.New(msg) - testutil.Equals(t, msg, err.Error()) - testutil.ErrorIs(t, apperr.Error{msg, nil}, err) - testutil.IsTrue(t, errors.As(err, &apperr.Error{})) + assert.Equal(t, msg, err.Error()) + assert.ErrorIs(t, apperr.Error{msg, nil}, err) + assert.True(t, errors.As(err, &apperr.Error{})) }) t.Run("could be instantiated with a detail error", func(t *testing.T) { err := errors.New("some infrastructure error") derr := apperr.NewWithDetail("some_code", err) - testutil.Equals(t, `some_code:some infrastructure error`, derr.Error()) - testutil.ErrorIs(t, apperr.Error{"some_code", err}, derr) - testutil.ErrorIs(t, err, derr) + assert.Equal(t, `some_code:some infrastructure error`, derr.Error()) + assert.ErrorIs(t, apperr.Error{"some_code", err}, derr) + assert.ErrorIs(t, err, derr) }) t.Run("implements the Is function for nested errors", func(t *testing.T) { err := apperr.New("some_pouet") wrapped := apperr.Wrap(err, errors.New("some infrastructure error")) - testutil.ErrorIs(t, err, wrapped) + assert.ErrorIs(t, err, wrapped) }) } func Test_Wrap(t *testing.T) { - t.Run("should populate the Detail field of a Error", func(t *testing.T) { + t.Run("should populate the Detail field of an Error", func(t *testing.T) { err := apperr.New("some_code") detail := errors.New("another error") derr := apperr.Wrap(err, detail) - testutil.Equals(t, `some_code:another error`, derr.Error()) - testutil.ErrorIs(t, apperr.Error{"some_code", detail}, derr) + assert.Equal(t, `some_code:another error`, derr.Error()) + assert.ErrorIs(t, apperr.Error{"some_code", detail}, derr) }) t.Run("should create a new Error if err is not one", func(t *testing.T) { @@ -51,8 +51,8 @@ func Test_Wrap(t *testing.T) { detail := errors.New("another error") derr := apperr.Wrap(err, detail) - testutil.Equals(t, `some_code:another error`, derr.Error()) - testutil.ErrorIs(t, apperr.Error{"some_code", detail}, derr) + assert.Equal(t, `some_code:another error`, derr.Error()) + assert.ErrorIs(t, apperr.Error{"some_code", detail}, derr) }) } @@ -62,11 +62,11 @@ func Test_As(t *testing.T) { appErr, ok := apperr.As[apperr.Error](err) - testutil.IsTrue(t, ok) - testutil.Equals(t, "base app error", appErr.Error()) + assert.True(t, ok) + assert.Equal(t, "base app error", appErr.Error()) err = errors.New("another one") _, ok = apperr.As[apperr.Error](err) - testutil.IsFalse(t, ok) + assert.False(t, ok) }) } diff --git a/pkg/assert/assert.go b/pkg/assert/assert.go new file mode 100644 index 00000000..a2aa2d6e --- /dev/null +++ b/pkg/assert/assert.go @@ -0,0 +1,220 @@ +package assert + +import ( + "errors" + "fmt" + "os" + "reflect" + "regexp" + "testing" + "unicode/utf8" + + "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/event" + "github.com/YuukanOO/seelf/pkg/validate" +) + +// Asserts that the given value is true +func True[T ~bool](t testing.TB, actual T, formatAndMessage ...any) { + if actual { + return + } + + failed(t, "should have been true", true, actual, formatAndMessage) +} + +// Asserts that the given value is false +func False[T ~bool](t testing.TB, actual T, formatAndMessage ...any) { + if !actual { + return + } + + failed(t, "should have been false", false, actual, formatAndMessage) +} + +// Asserts that the given value is nil +func Nil(t testing.TB, actual any, formatAndMessage ...any) { + if actual == nil { + return + } + + failed(t, "should have been nil", nil, actual, formatAndMessage) +} + +// Asserts that the given value is not nil +func NotNil(t testing.TB, actual any, formatAndMessage ...any) { + if actual != nil { + return + } + + failed(t, "should have been not nil", "nothing but ", actual, formatAndMessage) +} + +// Asserts that the given values are equal +func Equal[T comparable](t testing.TB, expected, actual T, formatAndMessage ...any) { + if expected == actual { + return + } + + failed(t, "should have been equal", expected, actual, formatAndMessage) +} + +// Asserts that the given values are not equal +func NotEqual[T comparable](t testing.TB, expected, actual T, formatAndMessage ...any) { + if expected != actual { + return + } + + failed(t, "should not have been equal", expected, actual, formatAndMessage) +} + +// Asserts that the given values are deeply equal using the reflect.DeepEqual function +func DeepEqual[T any](t testing.TB, expected, actual T, formatAndMessage ...any) { + if reflect.DeepEqual(expected, actual) { + return + } + + failed(t, "should have been deeply equal", expected, actual, formatAndMessage) +} + +// Asserts that the given value is of the given type and returns it. +func Is[T any](t testing.TB, actual any, formatAndMessage ...any) T { + result, ok := actual.(T) + + if ok { + return result + } + + failed(t, "wrong type", reflect.TypeOf(result).String(), reflect.TypeOf(actual).String(), formatAndMessage) + + return result +} + +// Asserts that the given error is the expected error using the function errors.Is +func ErrorIs(t testing.TB, expected, actual error, formatAndMessage ...any) { + if errors.Is(actual, expected) { + return + } + + failed(t, "errors should have match", expected, actual, formatAndMessage) +} + +// Asserts that the actual slice has the expected length +func HasLength[T any](t testing.TB, expected int, actual []T, formatAndMessage ...any) { + got := len(actual) + + if got == expected { + return + } + + failed(t, "should have correct length", expected, got, formatAndMessage) +} + +// Asserts that the actual string has the expected number of utf8 runes +func HasNRunes[T ~string](t testing.TB, expected int, actual T, formatAndMessage ...any) { + got := utf8.RuneCountInString(string(actual)) + + if got == expected { + return + } + + failed(t, "should have correct number of characters", expected, got, formatAndMessage) +} + +// Asserts that the actual source has the expected number of events +func HasNEvents[T event.Source](t testing.TB, expected int, source T, formatAndMessage ...any) { + got := len(event.Unwrap(source)) + + if got == expected { + return + } + + failed(t, "should have correct number of events", expected, got, formatAndMessage) +} + +// Asserts that the actual source has the expected event type at the given index and returns it +func EventIs[T event.Event](t testing.TB, source event.Source, index int, formatAndMessage ...any) T { + events := event.Unwrap(source) + + if index >= len(events) { + failed(t, "could not find an event at given index", index, len(events), formatAndMessage) + var r T + return r + } + + return Is[T](t, events[index], formatAndMessage...) +} + +// Asserts that the actual error is a validation error with the expected field errors +func ValidationError(t testing.TB, expected validate.FieldErrors, actual error, formatAndMessage ...any) { + ErrorIs(t, validate.ErrValidationFailed, actual, formatAndMessage...) + + fields, ok := apperr.As[validate.FieldErrors](actual) + + if !ok { + failed(t, "wrong error type", reflect.TypeOf(expected).String(), reflect.TypeOf(actual).String(), formatAndMessage) + return + } + + DeepEqual(t, expected, fields, formatAndMessage...) +} + +// Asserts that the given value is the zero value for the corresponding type +func Zero[T comparable](t testing.TB, actual T, formatAndMessage ...any) T { + var zero T + + if actual == zero { + return actual + } + + failed(t, "should be zero", zero, actual, formatAndMessage) + + return actual +} + +// Asserts that the given value is not the zero value for the corresponding type and returns it +func NotZero[T comparable](t testing.TB, actual T, formatAndMessage ...any) T { + var zero T + + if actual != zero { + return actual + } + + failed(t, "should not be zero", "anything but the zero value", actual, formatAndMessage) + + return actual +} + +// Asserts that the given value matches the expected regular expression +func Match(t testing.TB, expectedRegexp string, value string, formatAndMessage ...any) { + if regexp.MustCompile(expectedRegexp).MatchString(value) { + return + } + + failed(t, "should match", expectedRegexp, value, formatAndMessage) +} + +// Asserts that the file at the given path contains the expected content +func FileContentEquals(t testing.TB, expectedContent string, path string, formatAndMessage ...any) { + data, _ := os.ReadFile(path) + str := string(data) + + if str == expectedContent { + return + } + + failed(t, "should contains", expectedContent, str, formatAndMessage) +} + +func failed(t testing.TB, msg string, expected, actual any, contextMessage []any) { + if len(contextMessage) > 0 { + msg = fmt.Sprintf("%s - %s", msg, fmt.Sprintf(contextMessage[0].(string), contextMessage[1:]...)) + } + + t.Errorf(`%s + expected: +%#v + + got: +%#v`, msg, expected, actual) +} diff --git a/pkg/assert/assert_test.go b/pkg/assert/assert_test.go new file mode 100644 index 00000000..20e8b540 --- /dev/null +++ b/pkg/assert/assert_test.go @@ -0,0 +1,589 @@ +package assert_test + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/YuukanOO/seelf/pkg/assert" + "github.com/YuukanOO/seelf/pkg/bus" + "github.com/YuukanOO/seelf/pkg/event" + "github.com/YuukanOO/seelf/pkg/validate" + "github.com/YuukanOO/seelf/pkg/validate/numbers" + "github.com/YuukanOO/seelf/pkg/validate/strings" +) + +func Test_True(t *testing.T) { + t.Run("should correctly fail given a false value", func(t *testing.T) { + mock := new(mockT) + + assert.True(mock, false, "with value %s", "false") + + shouldHaveFailed(t, mock, `should have been true - with value false + expected: +true + + got: +false`) + }) + + t.Run("should correctly pass given a true value", func(t *testing.T) { + mock := new(mockT) + + assert.True(mock, true, "with value %s", "true") + + shouldHaveSucceeded(t, mock) + }) +} + +func Test_False(t *testing.T) { + t.Run("should correctly fail given a true value", func(t *testing.T) { + mock := new(mockT) + + assert.False(mock, true, "with value %s", "true") + + shouldHaveFailed(t, mock, `should have been false - with value true + expected: +false + + got: +true`) + }) + + t.Run("should correctly pass given a false value", func(t *testing.T) { + mock := new(mockT) + + assert.False(mock, false, "with value %s", "false") + + shouldHaveSucceeded(t, mock) + }) +} + +func Test_Nil(t *testing.T) { + t.Run("should correctly fail given a non nil value", func(t *testing.T) { + mock := new(mockT) + + assert.Nil(mock, "a string", "with a non nil value") + + shouldHaveFailed(t, mock, `should have been nil - with a non nil value + expected: + + + got: +"a string"`) + }) + + t.Run("should correctly pass given a nil value", func(t *testing.T) { + mock := new(mockT) + + assert.Nil(mock, nil, "with a nil value") + + shouldHaveSucceeded(t, mock) + }) +} + +func Test_NotNil(t *testing.T) { + t.Run("should correctly fail given a nil value", func(t *testing.T) { + mock := new(mockT) + + assert.NotNil(mock, nil, "with a nil value") + + shouldHaveFailed(t, mock, `should have been not nil - with a nil value + expected: +"nothing but " + + got: +`) + }) + + t.Run("should correctly pass given a non nil value", func(t *testing.T) { + mock := new(mockT) + + assert.NotNil(mock, "a string", "with a non nil value") + + shouldHaveSucceeded(t, mock) + }) +} + +func Test_Equal(t *testing.T) { + t.Run("should correctly fail given different values", func(t *testing.T) { + mock := new(mockT) + + assert.Equal(mock, true, false, "with different values") + + shouldHaveFailed(t, mock, `should have been equal - with different values + expected: +true + + got: +false`) + }) + + t.Run("should correctly pass given the expected value", func(t *testing.T) { + mock := new(mockT) + + assert.Equal(mock, true, true, "with same values") + + shouldHaveSucceeded(t, mock) + }) +} + +func Test_NotEqual(t *testing.T) { + t.Run("should correctly fail given the expected value", func(t *testing.T) { + mock := new(mockT) + + assert.NotEqual(mock, true, true, "with same values") + + shouldHaveFailed(t, mock, `should not have been equal - with same values + expected: +true + + got: +true`) + }) + + t.Run("should correctly pass given different values", func(t *testing.T) { + mock := new(mockT) + + assert.NotEqual(mock, true, false, "with different values") + + shouldHaveSucceeded(t, mock) + }) +} + +func Test_DeepEqual(t *testing.T) { + t.Run("should correctly fail given different slices", func(t *testing.T) { + mock := new(mockT) + + assert.DeepEqual(mock, []int{1}, []int{2}, "with different slices") + + shouldHaveFailed(t, mock, `should have been deeply equal - with different slices + expected: +[]int{1} + + got: +[]int{2}`) + }) + + t.Run("should correctly pass given the same slice", func(t *testing.T) { + mock := new(mockT) + + assert.DeepEqual(mock, []int{1}, []int{1}, "with the same slice") + + shouldHaveSucceeded(t, mock) + }) + + t.Run("should correctly pass given the same struct", func(t *testing.T) { + mock := new(mockT) + + assert.DeepEqual(mock, struct { + foo string + bar int + }{foo: "bar", bar: 42}, struct { + foo string + bar int + }{foo: "bar", bar: 42}, "with the same struct") + + shouldHaveSucceeded(t, mock) + }) + + t.Run("should correctly fail given different structs", func(t *testing.T) { + mock := new(mockT) + + assert.DeepEqual(mock, struct { + foo string + bar int + }{foo: "bar", bar: 42}, struct { + foo string + bar int + }{foo: "bar", bar: 24}, "with different structs") + + shouldHaveFailed(t, mock, `should have been deeply equal - with different structs + expected: +struct { foo string; bar int }{foo:"bar", bar:42} + + got: +struct { foo string; bar int }{foo:"bar", bar:24}`) + }) +} + +func Test_Is(t *testing.T) { + t.Run("should correctly fail given the wrong type", func(t *testing.T) { + mock := new(mockT) + + result := assert.Is[string](mock, 5, "with wrong type") + + shouldHaveFailed(t, mock, `wrong type - with wrong type + expected: +"string" + + got: +"int"`) + + if result != "" { + t.Error("result should be empty") + } + }) + + t.Run("should correctly pass given the right type", func(t *testing.T) { + mock := new(mockT) + + result := assert.Is[string](mock, "test", "with right type") + + shouldHaveSucceeded(t, mock) + + if result != "test" { + t.Error("result should be 'test'") + } + }) +} + +func Test_ErrorIs(t *testing.T) { + t.Run("should correctly fail given a wrong error", func(t *testing.T) { + mock := new(mockT) + + assert.ErrorIs(mock, errors.New("test"), errors.New("another err"), "with wrong error") + + shouldHaveFailed(t, mock, `errors should have match - with wrong error + expected: +&errors.errorString{s:"test"} + + got: +&errors.errorString{s:"another err"}`) + }) + + t.Run("should correctly pass given a right error", func(t *testing.T) { + mock := new(mockT) + expectedErr := errors.New("test") + actualErr := fmt.Errorf("with wrapped error %w", expectedErr) + + assert.ErrorIs(mock, expectedErr, actualErr, "with right error") + + shouldHaveSucceeded(t, mock) + }) +} + +func Test_HasLength(t *testing.T) { + t.Run("should correctly fail given a wrong length", func(t *testing.T) { + mock := new(mockT) + + assert.HasLength(mock, 5, []int{1, 2, 3}, "with wrong length") + + shouldHaveFailed(t, mock, `should have correct length - with wrong length + expected: +5 + + got: +3`) + }) + + t.Run("should correctly pass given a right length", func(t *testing.T) { + mock := new(mockT) + + assert.HasLength(mock, 3, []int{1, 2, 3}, "with right length") + + shouldHaveSucceeded(t, mock) + }) +} + +func Test_HasNRunes(t *testing.T) { + t.Run("should correctly fail given a wrong length", func(t *testing.T) { + mock := new(mockT) + + assert.HasNRunes(mock, 5, "test", "with wrong length") + + shouldHaveFailed(t, mock, `should have correct number of characters - with wrong length + expected: +5 + + got: +4`) + }) + + t.Run("should correctly pass given a right length", func(t *testing.T) { + mock := new(mockT) + + assert.HasNRunes(mock, 4, "test", "with right length") + + shouldHaveSucceeded(t, mock) + }) +} + +type ( + eventA struct { + bus.Notification + value string + } + + eventB struct { + bus.Notification + value int + } + + entity struct { + event.Emitter + } +) + +func (event eventA) Name_() string { return "eventA" } +func (event eventB) Name_() string { return "eventB" } + +func Test_HasNEvents(t *testing.T) { + ent := entity{} + event.Store(&ent, eventA{}, eventB{}) + + t.Run("should correctly fail given a wrong length", func(t *testing.T) { + mock := new(mockT) + + assert.HasNEvents(mock, 1, &ent, "with wrong length") + + shouldHaveFailed(t, mock, `should have correct number of events - with wrong length + expected: +1 + + got: +2`) + }) + + t.Run("should correctly pass given a right length", func(t *testing.T) { + mock := new(mockT) + + assert.HasNEvents(mock, 2, &ent, "with right length") + + shouldHaveSucceeded(t, mock) + }) +} + +func Test_EventIs(t *testing.T) { + ent := entity{} + a := eventA{value: "value"} + b := eventB{value: 42} + event.Store(&ent, a, b) + + t.Run("should fail if index is out of range", func(t *testing.T) { + mock := new(mockT) + + result := assert.EventIs[eventA](mock, &ent, 2, "with wrong length") + + shouldHaveFailed(t, mock, `could not find an event at given index - with wrong length + expected: +2 + + got: +2`) + + if result == a { + t.Error("result should be empty") + } + }) + + t.Run("should fail if requested event type is wrong", func(t *testing.T) { + mock := new(mockT) + + result := assert.EventIs[eventB](mock, &ent, 0, "with wrong event type") + + shouldHaveFailed(t, mock, `wrong type - with wrong event type + expected: +"assert_test.eventB" + + got: +"assert_test.eventA"`) + + if result == b { + t.Error("result should be empty") + } + }) + + t.Run("should pass if requested event type is right", func(t *testing.T) { + mock := new(mockT) + + result := assert.EventIs[eventA](mock, &ent, 0, "with right event type") + + shouldHaveSucceeded(t, mock) + + if result != a { + t.Error("result should be equal to a") + } + }) +} + +func Test_ValidationError(t *testing.T) { + t.Run("should fail if the error is not a validation one", func(t *testing.T) { + mock := new(mockT) + err := errors.New("test") + + assert.ValidationError(mock, validate.FieldErrors{}, err, "with wrong error type") + + shouldHaveFailed(t, mock, `wrong error type - with wrong error type + expected: +"validate.FieldErrors" + + got: +"*errors.errorString"`) + }) + + t.Run("should fail if FieldErrors do not match", func(t *testing.T) { + mock := new(mockT) + err := validate.NewError(validate.FieldErrors{ + "a": numbers.ErrMin, + "b": strings.ErrRequired, + }) + + assert.ValidationError(mock, validate.FieldErrors{ + "a": strings.ErrRequired, + "b": numbers.ErrMin, + }, err, "with wrong FieldErrors") + + shouldHaveFailed(t, mock, `should have been deeply equal - with wrong FieldErrors + expected: +validate.FieldErrors{"a":apperr.Error{Code:"required", Detail:error(nil)}, "b":apperr.Error{Code:"min", Detail:error(nil)}} + + got: +validate.FieldErrors{"a":apperr.Error{Code:"min", Detail:error(nil)}, "b":apperr.Error{Code:"required", Detail:error(nil)}}`) + }) + + t.Run("should pass if FieldErrors match", func(t *testing.T) { + mock := new(mockT) + err := validate.NewError(validate.FieldErrors{ + "a": numbers.ErrMin, + "b": strings.ErrRequired, + }) + + assert.ValidationError(mock, validate.FieldErrors{ + "a": numbers.ErrMin, + "b": strings.ErrRequired, + }, err, "with right FieldErrors") + + shouldHaveSucceeded(t, mock) + }) +} + +func Test_Zero(t *testing.T) { + t.Run("should fail if the value is not the default one", func(t *testing.T) { + mock := new(mockT) + + result := assert.Zero(mock, "test", "with a string") + + shouldHaveFailed(t, mock, `should be zero - with a string + expected: +"" + + got: +"test"`) + + if result != "test" { + t.Error("result should be equal to the given value") + } + }) + + t.Run("should pass if the value is the default one", func(t *testing.T) { + mock := new(mockT) + + result := assert.Zero(mock, "", "with an empty string") + + shouldHaveSucceeded(t, mock) + + if result != "" { + t.Error("result should be empty") + } + }) +} + +func Test_NotZero(t *testing.T) { + t.Run("should fail if the value is the default one for simple types", func(t *testing.T) { + mock := new(mockT) + + result := assert.NotZero(mock, "", "with an empty string") + + shouldHaveFailed(t, mock, `should not be zero - with an empty string + expected: +"anything but the zero value" + + got: +""`) + + if result != "" { + t.Error("result should be empty") + } + }) + + t.Run("should pass if the value is not the default one for simple types", func(t *testing.T) { + mock := new(mockT) + + result := assert.NotZero(mock, "test", "with a string") + + shouldHaveSucceeded(t, mock) + + if result != "test" { + t.Error("result should be equal to the given value") + } + }) + + t.Run("should fail if the value is the default one for complex types", func(t *testing.T) { + mock := new(mockT) + var time time.Time + + result := assert.NotZero(mock, time, "with a time.Time value") + + shouldHaveFailed(t, mock, `should not be zero - with a time.Time value + expected: +"anything but the zero value" + + got: +time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)`) + + if result != time { + t.Error("result should be empty") + } + }) + + t.Run("should pass if the value is not the default one for complex types", func(t *testing.T) { + mock := new(mockT) + time := time.Now().UTC() + + result := assert.NotZero(mock, time, "with a time.Time value") + + shouldHaveSucceeded(t, mock) + + if result != time { + t.Error("result should be equal to the given value") + } + }) +} + +type mockT struct { + testing.TB + hasFailed bool + msg string +} + +func (t *mockT) Errorf(format string, args ...any) { + t.hasFailed = true + t.msg = fmt.Sprintf(format, args...) +} + +func shouldHaveFailed(t testing.TB, mock *mockT, expectedMessage string) { + if !mock.hasFailed { + t.Error("should have failed") + } + + if mock.msg != expectedMessage { + t.Errorf(`message should have matched: +expected: + %s + +got: + %s`, expectedMessage, mock.msg) + } +} + +func shouldHaveSucceeded(t testing.TB, mock *mockT) { + if mock.hasFailed { + t.Error("should not have failed") + } + + if mock.msg != "" { + t.Error("message should be empty") + } +} diff --git a/pkg/bus/dispatcher.go b/pkg/bus/dispatcher.go index ed1d876c..4cf2d7fb 100644 --- a/pkg/bus/dispatcher.go +++ b/pkg/bus/dispatcher.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/YuukanOO/seelf/pkg/storage" + "github.com/YuukanOO/seelf/pkg/types" ) var ErrNoHandlerRegistered = errors.New("no_handler_registered") @@ -30,7 +31,10 @@ type ( } ) -// Register an handler for a specific request on the provided bus. +// Register an handler for a specific request on the provided bus. You should always +// prefer this registration method. +// If the provided message is an async one, it will be automatically registered on +// the Marshallable mapper to make things easier. func Register[TResult any, TMsg TypedRequest[TResult]](bus Bus, handler RequestHandler[TResult, TMsg]) { var ( msg TMsg @@ -42,8 +46,12 @@ func Register[TResult any, TMsg TypedRequest[TResult]](bus Bus, handler RequestH bus.Register(msg, h) // If the message is schedulable, register the unmarshaller automatically. - if _, isSchedulable := any(msg).(Schedulable); isSchedulable { - Marshallable.Register(msg, func(s string) (Request, error) { return storage.UnmarshalJSON[TMsg](s) }) + // This is done here because of the known type TMsg but maybe I should try to + // move it to bus/memory in the future. + if types.Is[Schedulable](msg) { + Marshallable.Register(msg, func(s string) (Request, error) { + return storage.UnmarshalJSON[TMsg](s) + }) } } diff --git a/pkg/bus/memory/dispatcher.go b/pkg/bus/memory/dispatcher.go index a4beac75..64c58197 100644 --- a/pkg/bus/memory/dispatcher.go +++ b/pkg/bus/memory/dispatcher.go @@ -4,6 +4,7 @@ import ( "context" "github.com/YuukanOO/seelf/pkg/bus" + "github.com/YuukanOO/seelf/pkg/types" ) type ( @@ -28,12 +29,12 @@ func (b *dispatcher) Register(msg bus.Message, handler bus.NextFunc) { name := msg.Name_() _, exists := b.handlers[name] - // Apply middlewares to avoid doing it at runtime + // Apply middlewares here to avoid doing it at runtime for i := len(b.middlewares) - 1; i >= 0; i-- { handler = b.middlewares[i](handler) } - if msg.Kind_() == bus.MessageKindNotification { + if types.Is[bus.Signal](msg) { if !exists { b.handlers[name] = []bus.NextFunc{handler} } else { @@ -61,15 +62,15 @@ func (b *dispatcher) Send(ctx context.Context, msg bus.Request) (any, error) { func (b *dispatcher) Notify(ctx context.Context, msgs ...bus.Signal) error { for _, msg := range msgs { - handlers := b.handlers[msg.Name_()] + value := b.handlers[msg.Name_()] - if handlers == nil { + if value == nil { continue } - hdls := handlers.([]bus.NextFunc) + handlers := value.([]bus.NextFunc) - for _, h := range hdls { + for _, h := range handlers { _, err := h(ctx, msg) if err != nil { diff --git a/pkg/bus/memory/dispatcher_test.go b/pkg/bus/memory/dispatcher_test.go index c94fa111..b7bbb02b 100644 --- a/pkg/bus/memory/dispatcher_test.go +++ b/pkg/bus/memory/dispatcher_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" "github.com/YuukanOO/seelf/pkg/bus/memory" - "github.com/YuukanOO/seelf/pkg/testutil" ) func TestBus(t *testing.T) { @@ -38,7 +38,7 @@ func TestBus(t *testing.T) { _, err := bus.Send(local, context.Background(), &addCommand{}) - testutil.ErrorIs(t, bus.ErrNoHandlerRegistered, err) + assert.ErrorIs(t, bus.ErrNoHandlerRegistered, err) }) t.Run("should returns the request handler error back if any", func(t *testing.T) { @@ -51,7 +51,7 @@ func TestBus(t *testing.T) { _, err := bus.Send(local, context.Background(), addCommand{}) - testutil.ErrorIs(t, expectedErr, err) + assert.ErrorIs(t, expectedErr, err) }) t.Run("should call the appropriate request handler and returns the result", func(t *testing.T) { @@ -64,13 +64,13 @@ func TestBus(t *testing.T) { result, err := bus.Send(local, context.Background(), addCommand{A: 1, B: 2}) - testutil.IsNil(t, err) - testutil.Equals(t, 3, result) + assert.Nil(t, err) + assert.Equal(t, 3, result) result, err = bus.Send(local, context.Background(), getQuery{}) - testutil.IsNil(t, err) - testutil.Equals(t, 42, result) + assert.Nil(t, err) + assert.Equal(t, 42, result) }) t.Run("should do nothing if no signal handler is registered for a given signal", func(t *testing.T) { @@ -78,7 +78,7 @@ func TestBus(t *testing.T) { err := local.Notify(context.Background(), registeredNotification{}) - testutil.IsNil(t, err) + assert.Nil(t, err) }) t.Run("should returns a signal handler error back if any", func(t *testing.T) { @@ -95,7 +95,7 @@ func TestBus(t *testing.T) { err := local.Notify(context.Background(), registeredNotification{}) - testutil.ErrorIs(t, expectedErr, err) + assert.ErrorIs(t, expectedErr, err) }) t.Run("should call every signal handlers registered for the given signal", func(t *testing.T) { @@ -117,8 +117,8 @@ func TestBus(t *testing.T) { err := local.Notify(context.Background(), registeredNotification{}) - testutil.IsNil(t, err) - testutil.IsTrue(t, firstOneCalled && secondOneCalled) + assert.Nil(t, err) + assert.True(t, firstOneCalled && secondOneCalled) }) t.Run("should call every middlewares registered", func(t *testing.T) { @@ -153,16 +153,16 @@ func TestBus(t *testing.T) { B: 2, }) - testutil.IsNil(t, err) - testutil.Equals(t, 3, r) - testutil.DeepEquals(t, []int{1, 2, 2, 1}, calls) + assert.Nil(t, err) + assert.Equal(t, 3, r) + assert.DeepEqual(t, []int{1, 2, 2, 1}, calls) calls = make([]int, 0) - local.Notify(context.Background(), registeredNotification{}) + assert.Nil(t, local.Notify(context.Background(), registeredNotification{})) // Should have been called twice cuz 2 signal handlers are registered - testutil.DeepEquals(t, []int{1, 2, 2, 1, 1, 2, 2, 1}, calls) + assert.DeepEqual(t, []int{1, 2, 2, 1, 1, 2, 2, 1}, calls) }) } diff --git a/pkg/bus/message_test.go b/pkg/bus/message_test.go index fa639ec4..a445d079 100644 --- a/pkg/bus/message_test.go +++ b/pkg/bus/message_test.go @@ -3,8 +3,8 @@ package bus_test import ( "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/testutil" ) func TestMessage(t *testing.T) { @@ -15,9 +15,9 @@ func TestMessage(t *testing.T) { notif registeredNotification ) - testutil.Equals(t, bus.MessageKindCommand, command.Kind_()) - testutil.Equals(t, bus.MessageKindQuery, query.Kind_()) - testutil.Equals(t, bus.MessageKindNotification, notif.Kind_()) + assert.Equal(t, bus.MessageKindCommand, command.Kind_()) + assert.Equal(t, bus.MessageKindQuery, query.Kind_()) + assert.Equal(t, bus.MessageKindNotification, notif.Kind_()) }) } diff --git a/pkg/bus/scheduler.go b/pkg/bus/scheduler.go index 954b1358..f37d3180 100644 --- a/pkg/bus/scheduler.go +++ b/pkg/bus/scheduler.go @@ -10,8 +10,6 @@ import ( "github.com/YuukanOO/seelf/pkg/storage" ) -var _ Scheduler = (*defaultScheduler)(nil) // Validate interface implementation - const ( JobPolicyRetryPreserveOrder JobPolicy = 1 << iota // Retry the job but preserve the order among the group JobPolicyWaitForOthersResourceID // Wait for other jobs on the same resource id to finish before processing diff --git a/pkg/bus/scheduler_test.go b/pkg/bus/scheduler_test.go index cc161a67..ec5ed897 100644 --- a/pkg/bus/scheduler_test.go +++ b/pkg/bus/scheduler_test.go @@ -8,13 +8,13 @@ import ( "sync" "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" "github.com/YuukanOO/seelf/pkg/bus/memory" "github.com/YuukanOO/seelf/pkg/flag" "github.com/YuukanOO/seelf/pkg/log" "github.com/YuukanOO/seelf/pkg/must" "github.com/YuukanOO/seelf/pkg/storage" - "github.com/YuukanOO/seelf/pkg/testutil" ) func TestScheduler(t *testing.T) { @@ -41,35 +41,35 @@ func TestScheduler(t *testing.T) { withUnwrapedErr := returnCommand{err: innerErr} withPreservedOrderErr := returnCommand{err: innerErr} - testutil.IsNil(t, scheduler.Queue(context.Background(), withoutErr)) - testutil.IsNil(t, scheduler.Queue(context.Background(), withUnwrapedErr)) - testutil.IsNil(t, scheduler.Queue(context.Background(), withPreservedOrderErr, bus.WithPolicy(bus.JobPolicyRetryPreserveOrder))) - testutil.IsNil(t, scheduler.Queue(context.Background(), addCommand{})) + assert.Nil(t, scheduler.Queue(context.Background(), withoutErr)) + assert.Nil(t, scheduler.Queue(context.Background(), withUnwrapedErr)) + assert.Nil(t, scheduler.Queue(context.Background(), withPreservedOrderErr, bus.WithPolicy(bus.JobPolicyRetryPreserveOrder))) + assert.Nil(t, scheduler.Queue(context.Background(), addCommand{})) adapter.wait() - testutil.HasLength(t, adapter.done, 1) + assert.HasLength(t, 1, adapter.done) slices.SortFunc(adapter.done, func(a, b *job) int { return a.id - b.id }) - testutil.Equals(t, 0, adapter.done[0].id) + assert.Equal(t, 0, adapter.done[0].id) - testutil.HasLength(t, adapter.retried, 3) + assert.HasLength(t, 3, adapter.retried) slices.SortFunc(adapter.retried, func(a, b *job) int { return a.id - b.id }) - testutil.Equals(t, 1, adapter.retried[0].id) - testutil.ErrorIs(t, innerErr, adapter.retried[0].err) - testutil.IsFalse(t, adapter.retried[0].preserveOrder) + assert.Equal(t, 1, adapter.retried[0].id) + assert.ErrorIs(t, innerErr, adapter.retried[0].err) + assert.False(t, adapter.retried[0].preserveOrder) - testutil.Equals(t, 2, adapter.retried[1].id) - testutil.ErrorIs(t, innerErr, adapter.retried[1].err) - testutil.IsTrue(t, adapter.retried[1].preserveOrder) + assert.Equal(t, 2, adapter.retried[1].id) + assert.ErrorIs(t, innerErr, adapter.retried[1].err) + assert.True(t, adapter.retried[1].preserveOrder) - testutil.Equals(t, 3, adapter.retried[2].id) - testutil.ErrorIs(t, bus.ErrNoHandlerRegistered, adapter.retried[2].err) + assert.Equal(t, 3, adapter.retried[2].id) + assert.ErrorIs(t, bus.ErrNoHandlerRegistered, adapter.retried[2].err) }) } diff --git a/pkg/bus/spy/dispatcher.go b/pkg/bus/spy/dispatcher.go new file mode 100644 index 00000000..75764fb2 --- /dev/null +++ b/pkg/bus/spy/dispatcher.go @@ -0,0 +1,48 @@ +//go:build !release + +package spy + +import ( + "context" + + "github.com/YuukanOO/seelf/pkg/bus" +) + +type ( + Dispatcher interface { + bus.Dispatcher + + Reset() // Clear all requests and signals + Requests() []bus.Request + Signals() []bus.Signal + } + + dispatcher struct { + requests []bus.Request + signals []bus.Signal + } +) + +// Builds a new dispatcher used for testing only. It will not send anything but +// append the requests and signals to the internal slices so they can be checked. +func NewDispatcher() Dispatcher { + return &dispatcher{} +} + +func (d *dispatcher) Send(ctx context.Context, msg bus.Request) (any, error) { + d.requests = append(d.requests, msg) + return nil, nil +} + +func (d *dispatcher) Notify(ctx context.Context, msgs ...bus.Signal) error { + d.signals = append(d.signals, msgs...) + return nil +} + +func (d *dispatcher) Reset() { + d.requests = nil + d.signals = nil +} + +func (d *dispatcher) Requests() []bus.Request { return d.requests } +func (d *dispatcher) Signals() []bus.Signal { return d.signals } diff --git a/pkg/bus/sqlite/store.go b/pkg/bus/sqlite/store.go index 020b76f2..d55c21ba 100644 --- a/pkg/bus/sqlite/store.go +++ b/pkg/bus/sqlite/store.go @@ -2,9 +2,7 @@ package sqlite import ( "context" - "database/sql" "embed" - "errors" "time" "github.com/YuukanOO/seelf/pkg/apperr" @@ -21,7 +19,7 @@ var ( //go:embed migrations/*.sql migrations embed.FS - migrationsModule = sqlite.NewMigrationsModule("scheduler", "migrations", migrations) + Migrations = sqlite.NewMigrationsModule("scheduler", "migrations", migrations) ) type ( @@ -67,7 +65,7 @@ func NewScheduledJobsStore(db *sqlite.Database) bus.ScheduledJobsStore { // them as not retrieved so they will be picked up next time GetNextPendingJobs is called. // You MUST call this method at the application startup. func (s *store) Setup() error { - if err := s.db.Migrate(migrationsModule); err != nil { + if err := s.db.Migrate(Migrations); err != nil { return err } @@ -99,18 +97,16 @@ func (s *store) Create( // Could not use the ON CONFLICT here :'( if flag.IsSet(options.Policy, bus.JobPolicyMerge) { - var existingJobId string - - if err = s.db.QueryRowContext(ctx, ` - SELECT id - FROM scheduled_jobs - WHERE resource_id = ? AND message_name = ? AND retrieved = false`, resourceId, msgName). - Scan(&existingJobId); err != nil && !errors.Is(err, sql.ErrNoRows) { - return err - } - - if existingJobId != "" { - _, err = s.db.ExecContext(ctx, `UPDATE scheduled_jobs SET message_data = ? WHERE id = ?`, msgValue, existingJobId) + result, err := s.db.ExecContext(ctx, ` + UPDATE scheduled_jobs + SET message_data = ? + WHERE id = ( + SELECT id + FROM scheduled_jobs + WHERE resource_id = ? AND message_name = ? AND retrieved = false + )`, msgValue, resourceId, msgName) + + if affected, _ := result.RowsAffected(); affected > 0 { return err } } diff --git a/pkg/config/loader_test.go b/pkg/config/loader_test.go index 7f1d5d45..d9ea5fe9 100644 --- a/pkg/config/loader_test.go +++ b/pkg/config/loader_test.go @@ -6,10 +6,10 @@ import ( "os" "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/config" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/ostools" - "github.com/YuukanOO/seelf/pkg/testutil" ) type ( @@ -149,20 +149,20 @@ HTTP_TWO=true`, if tt.conf != "" { err := ostools.WriteFile(confFilename, []byte(tt.conf)) - testutil.IsNil(t, err) + assert.Nil(t, err) } if tt.env != "" { err := ostools.WriteFile(envFilename, []byte(tt.env)) - testutil.IsNil(t, err) + assert.Nil(t, err) } var conf configuration exists, err := config.Load(confFilename, &conf, envFilename) - testutil.IsNil(t, err) - testutil.Equals(t, tt.conf != "", exists) - testutil.DeepEquals(t, tt.expected, conf) + assert.Nil(t, err) + assert.Equal(t, tt.conf != "", exists) + assert.DeepEqual(t, tt.expected, conf) }) } @@ -174,8 +174,8 @@ HTTP_TWO=true`, exists, err := config.Load(confFilename, &conf) - testutil.ErrorIs(t, errPostLoad, err) - testutil.IsFalse(t, exists) + assert.ErrorIs(t, errPostLoad, err) + assert.False(t, exists) }) } @@ -203,10 +203,10 @@ func Test_Save(t *testing.T) { err := config.Save(confFilename, conf) - testutil.IsNil(t, err) + assert.Nil(t, err) b, err := os.ReadFile(confFilename) - testutil.IsNil(t, err) - testutil.Equals(t, `verbose: true + assert.Nil(t, err) + assert.Equal(t, `verbose: true http: host: 127.0.0.1 secure: true diff --git a/pkg/crypto/random_test.go b/pkg/crypto/random_test.go index cc19d918..bb105f66 100644 --- a/pkg/crypto/random_test.go +++ b/pkg/crypto/random_test.go @@ -3,12 +3,12 @@ package crypto_test import ( "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/crypto" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_RandomKey(t *testing.T) { key, err := crypto.RandomKey[string](32) - testutil.IsNil(t, err) - testutil.HasNChars(t, 32, key) + assert.Nil(t, err) + assert.HasNRunes(t, 32, key) } diff --git a/pkg/domain/action_test.go b/pkg/domain/action_test.go index fdb6da3b..3805a432 100644 --- a/pkg/domain/action_test.go +++ b/pkg/domain/action_test.go @@ -4,9 +4,9 @@ import ( "testing" "time" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/domain" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) type userId string @@ -16,8 +16,8 @@ func Test_Action(t *testing.T) { var user userId = "john" act := domain.NewAction(user) - testutil.Equals(t, user, act.By()) - testutil.IsFalse(t, act.At().IsZero()) + assert.Equal(t, user, act.By()) + assert.False(t, act.At().IsZero()) }) t.Run("should be rehydrated with the From function", func(t *testing.T) { @@ -28,7 +28,7 @@ func Test_Action(t *testing.T) { act := domain.ActionFrom(user, at) - testutil.Equals(t, user, act.By()) - testutil.Equals(t, at, act.At()) + assert.Equal(t, user, act.By()) + assert.Equal(t, at, act.At()) }) } diff --git a/pkg/domain/interval_test.go b/pkg/domain/interval_test.go index dff2693f..0b8c4d1d 100644 --- a/pkg/domain/interval_test.go +++ b/pkg/domain/interval_test.go @@ -4,15 +4,15 @@ import ( "testing" "time" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/domain" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_TimeInterval(t *testing.T) { t.Run("should fail if the from date is after the to date", func(t *testing.T) { _, err := domain.NewTimeInterval(time.Now(), time.Now().Add(-time.Second)) - testutil.ErrorIs(t, domain.ErrInvalidTimeInterval, err) + assert.ErrorIs(t, domain.ErrInvalidTimeInterval, err) }) t.Run("should succeed if the from date is before the to date", func(t *testing.T) { @@ -20,8 +20,8 @@ func Test_TimeInterval(t *testing.T) { to := time.Now().Add(time.Second) ti, err := domain.NewTimeInterval(from, to) - testutil.IsNil(t, err) - testutil.Equals(t, from, ti.From()) - testutil.Equals(t, to, ti.To()) + assert.Nil(t, err) + assert.Equal(t, from, ti.From()) + assert.Equal(t, to, ti.To()) }) } diff --git a/pkg/event/event_test.go b/pkg/event/event_test.go index e42bc530..1bb9aa27 100644 --- a/pkg/event/event_test.go +++ b/pkg/event/event_test.go @@ -3,9 +3,9 @@ package event_test import ( "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/bus" "github.com/YuukanOO/seelf/pkg/event" - "github.com/YuukanOO/seelf/pkg/testutil" ) type ( @@ -34,9 +34,9 @@ func Test_Emitter(t *testing.T) { evts := event.Unwrap(&ent) - testutil.HasLength(t, evts, 2) - testutil.Equals(t, evt1, evts[0].(domainEventA)) - testutil.Equals(t, evt2, evts[1].(domainEventB)) + assert.HasLength(t, 2, evts) + assert.Equal(t, evt1, evts[0].(domainEventA)) + assert.Equal(t, evt2, evts[1].(domainEventB)) }) // t.Run("should be able to clear all events from an Emitter", func(t *testing.T) { diff --git a/pkg/flag/flag_test.go b/pkg/flag/flag_test.go index 54a9f4c3..95a1e633 100644 --- a/pkg/flag/flag_test.go +++ b/pkg/flag/flag_test.go @@ -3,8 +3,8 @@ package flag_test import ( "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/flag" - "github.com/YuukanOO/seelf/pkg/testutil" ) type flagType uint @@ -16,11 +16,11 @@ const ( ) func Test_IsSet(t *testing.T) { - testutil.IsTrue(t, flag.IsSet(flagA, flagA)) - testutil.IsFalse(t, flag.IsSet(flagA, flagB)) - testutil.IsTrue(t, flag.IsSet(flagA|flagB, flagA)) - testutil.IsTrue(t, flag.IsSet(flagA|flagB, flagB|flagA)) - testutil.IsTrue(t, flag.IsSet(flagA|flagB|flagC, flagB|flagA)) - testutil.IsFalse(t, flag.IsSet(flagA, flagB|flagA)) - testutil.IsFalse(t, flag.IsSet(flagA|flagC, flagB|flagA)) + assert.True(t, flag.IsSet(flagA, flagA)) + assert.False(t, flag.IsSet(flagA, flagB)) + assert.True(t, flag.IsSet(flagA|flagB, flagA)) + assert.True(t, flag.IsSet(flagA|flagB, flagB|flagA)) + assert.True(t, flag.IsSet(flagA|flagB|flagC, flagB|flagA)) + assert.False(t, flag.IsSet(flagA, flagB|flagA)) + assert.False(t, flag.IsSet(flagA|flagC, flagB|flagA)) } diff --git a/pkg/id/id_test.go b/pkg/id/id_test.go index 678a513e..6c773159 100644 --- a/pkg/id/id_test.go +++ b/pkg/id/id_test.go @@ -3,8 +3,8 @@ package id_test import ( "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/id" - "github.com/YuukanOO/seelf/pkg/testutil" ) type someDomainID string @@ -13,7 +13,7 @@ func Test_ID_GeneratesANonEmptyUniqueIdentifier(t *testing.T) { id1 := id.New[someDomainID]() id2 := id.New[someDomainID]() - testutil.HasNChars(t, 27, id1) - testutil.HasNChars(t, 27, id2) - testutil.NotEquals(t, id1, id2) + assert.HasNRunes(t, 27, id1) + assert.HasNRunes(t, 27, id2) + assert.NotEqual(t, id1, id2) } diff --git a/pkg/monad/maybe_test.go b/pkg/monad/maybe_test.go index 1e591d9b..bc295f10 100644 --- a/pkg/monad/maybe_test.go +++ b/pkg/monad/maybe_test.go @@ -4,8 +4,8 @@ import ( "testing" "time" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/monad" - "github.com/YuukanOO/seelf/pkg/testutil" "gopkg.in/yaml.v3" ) @@ -13,19 +13,19 @@ func Test_Maybe(t *testing.T) { t.Run("should have a default state without value", func(t *testing.T) { var m monad.Maybe[time.Time] - testutil.IsFalse(t, m.HasValue()) + assert.False(t, m.HasValue()) }) t.Run("could be created empty", func(t *testing.T) { m := monad.None[time.Time]() - testutil.IsFalse(t, m.HasValue()) + assert.False(t, m.HasValue()) }) t.Run("could be created with a defined value", func(t *testing.T) { m := monad.Value("ok") - testutil.Equals(t, "ok", m.MustGet()) - testutil.IsTrue(t, m.HasValue()) + assert.Equal(t, "ok", m.MustGet()) + assert.True(t, m.HasValue()) }) t.Run("could returns its internal value and a boolean indicating if it has been set", func(t *testing.T) { @@ -33,15 +33,15 @@ func Test_Maybe(t *testing.T) { value, hasValue := m.TryGet() - testutil.IsFalse(t, hasValue) - testutil.Equals(t, "", value) + assert.False(t, hasValue) + assert.Equal(t, "", value) m.Set("ok") value, hasValue = m.TryGet() - testutil.IsTrue(t, hasValue) - testutil.Equals(t, "ok", value) + assert.True(t, hasValue) + assert.Equal(t, "ok", value) }) t.Run("could be assigned a value", func(t *testing.T) { @@ -51,8 +51,8 @@ func Test_Maybe(t *testing.T) { ) m.Set(now) - testutil.Equals(t, now, m.MustGet()) - testutil.IsTrue(t, m.HasValue()) + assert.Equal(t, now, m.MustGet()) + assert.True(t, m.HasValue()) }) t.Run("could unset its value", func(t *testing.T) { @@ -60,14 +60,14 @@ func Test_Maybe(t *testing.T) { m.Unset() - testutil.IsFalse(t, m.HasValue()) + assert.False(t, m.HasValue()) }) t.Run("should panic if trying to access a value with MustGet", func(t *testing.T) { defer func() { err := recover() - testutil.IsNotNil(t, err) - testutil.Equals(t, "trying to access a monad's value but none is set", err.(string)) + assert.NotNil(t, err) + assert.Equal(t, "trying to access a monad's value but none is set", err.(string)) }() var m monad.Maybe[time.Time] @@ -80,7 +80,7 @@ func Test_Maybe(t *testing.T) { m := monad.Value(now) - testutil.Equals(t, now, m.MustGet()) + assert.Equal(t, now, m.MustGet()) }) t.Run("could returns its value or fallback if not set", func(t *testing.T) { @@ -89,8 +89,8 @@ func Test_Maybe(t *testing.T) { wValue = monad.Value("got a value") ) - testutil.Equals(t, "got a value", wValue.Get("default")) - testutil.Equals(t, "default", woValue.Get("default")) + assert.Equal(t, "got a value", wValue.Get("default")) + assert.Equal(t, "default", woValue.Get("default")) }) t.Run("should implements the valuer interface", func(t *testing.T) { @@ -98,15 +98,15 @@ func Test_Maybe(t *testing.T) { driverValue, err := m.Value() - testutil.IsNil(t, err) - testutil.IsNil(t, driverValue) + assert.Nil(t, err) + assert.Nil(t, driverValue) now := time.Now().UTC() m.Set(now) driverValue, err = m.Value() - testutil.IsNil(t, err) - testutil.IsTrue(t, driverValue == now) + assert.Nil(t, err) + assert.True(t, driverValue == now) }) t.Run("should implements the Scanner interface", func(t *testing.T) { @@ -114,14 +114,14 @@ func Test_Maybe(t *testing.T) { err := m.Scan(nil) - testutil.IsNil(t, err) - testutil.IsFalse(t, m.HasValue()) + assert.Nil(t, err) + assert.False(t, m.HasValue()) err = m.Scan("data") - testutil.IsNil(t, err) - testutil.IsTrue(t, m.HasValue()) - testutil.Equals(t, "data", m.MustGet()) + assert.Nil(t, err) + assert.True(t, m.HasValue()) + assert.Equal(t, "data", m.MustGet()) }) t.Run("should correctly marshal to json", func(t *testing.T) { @@ -129,15 +129,15 @@ func Test_Maybe(t *testing.T) { data, err := m.MarshalJSON() - testutil.IsNil(t, err) - testutil.Equals(t, "null", string(data)) + assert.Nil(t, err) + assert.Equal(t, "null", string(data)) m.Set("ok") data, err = m.MarshalJSON() - testutil.IsNil(t, err) - testutil.Equals(t, `"ok"`, string(data)) + assert.Nil(t, err) + assert.Equal(t, `"ok"`, string(data)) }) t.Run("should correctly marshal to yaml", func(t *testing.T) { @@ -145,17 +145,17 @@ func Test_Maybe(t *testing.T) { data, err := m.MarshalYAML() - testutil.IsNil(t, err) - testutil.IsTrue(t, m.IsZero()) - testutil.IsNil(t, data) + assert.Nil(t, err) + assert.True(t, m.IsZero()) + assert.Nil(t, data) m.Set("ok") data, err = m.MarshalYAML() - testutil.IsNil(t, err) - testutil.IsFalse(t, m.IsZero()) - testutil.Equals(t, "ok", data) + assert.Nil(t, err) + assert.False(t, m.IsZero()) + assert.Equal(t, "ok", data) }) t.Run("should correctly unmarshal from yaml", func(t *testing.T) { @@ -163,9 +163,9 @@ func Test_Maybe(t *testing.T) { err := m.UnmarshalYAML(&yaml.Node{Kind: yaml.ScalarNode, Value: "ok"}) - testutil.IsNil(t, err) - testutil.IsTrue(t, m.HasValue()) - testutil.Equals(t, "ok", m.MustGet()) + assert.Nil(t, err) + assert.True(t, m.HasValue()) + assert.Equal(t, "ok", m.MustGet()) }) t.Run("should correctly unmarshal from env variables", func(t *testing.T) { @@ -173,12 +173,12 @@ func Test_Maybe(t *testing.T) { err := m.UnmarshalEnvironmentValue("") - testutil.IsNil(t, err) - testutil.IsFalse(t, m.HasValue()) + assert.Nil(t, err) + assert.False(t, m.HasValue()) err = m.UnmarshalEnvironmentValue("ok") - testutil.IsNil(t, err) - testutil.IsTrue(t, m.HasValue()) - testutil.Equals(t, "ok", m.MustGet()) + assert.Nil(t, err) + assert.True(t, m.HasValue()) + assert.Equal(t, "ok", m.MustGet()) }) } diff --git a/pkg/monad/patch_test.go b/pkg/monad/patch_test.go index 9773c8a2..1bfba74e 100644 --- a/pkg/monad/patch_test.go +++ b/pkg/monad/patch_test.go @@ -4,34 +4,34 @@ import ( "encoding/json" "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/monad" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_Patch(t *testing.T) { t.Run("should default to a not set, empty value", func(t *testing.T) { var p monad.Patch[int] - testutil.IsFalse(t, p.IsSet()) - testutil.IsFalse(t, p.IsNil()) - testutil.IsFalse(t, p.HasValue()) + assert.False(t, p.IsSet()) + assert.False(t, p.IsNil()) + assert.False(t, p.HasValue()) }) t.Run("should be instantiable with a value", func(t *testing.T) { p := monad.PatchValue(42) - testutil.IsTrue(t, p.IsSet()) - testutil.IsFalse(t, p.IsNil()) - testutil.IsTrue(t, p.HasValue()) - testutil.Equals(t, 42, p.MustGet()) + assert.True(t, p.IsSet()) + assert.False(t, p.IsNil()) + assert.True(t, p.HasValue()) + assert.Equal(t, 42, p.MustGet()) }) t.Run("should be instantiable with a nil value", func(t *testing.T) { p := monad.Nil[int]() - testutil.IsTrue(t, p.IsSet()) - testutil.IsTrue(t, p.IsNil()) - testutil.IsFalse(t, p.HasValue()) + assert.True(t, p.IsSet()) + assert.True(t, p.IsNil()) + assert.False(t, p.HasValue()) }) t.Run("should return the inner monad and a boolean indicating if it has been set", func(t *testing.T) { @@ -50,8 +50,8 @@ func Test_Patch(t *testing.T) { t.Run(test.name, func(t *testing.T) { m, isSet := test.value.TryGet() - testutil.Equals(t, test.isSet, isSet) - testutil.Equals(t, test.hasValue, m.HasValue()) + assert.Equal(t, test.isSet, isSet) + assert.Equal(t, test.hasValue, m.HasValue()) }) } }) @@ -72,10 +72,10 @@ func Test_Patch(t *testing.T) { t.Run(test.json, func(t *testing.T) { var value someStruct - testutil.IsNil(t, json.Unmarshal([]byte(test.json), &value)) - testutil.Equals(t, test.isSet, value.Number.IsSet()) - testutil.Equals(t, test.isNil, value.Number.IsNil()) - testutil.Equals(t, test.hasValue, value.Number.HasValue()) + assert.Nil(t, json.Unmarshal([]byte(test.json), &value)) + assert.Equal(t, test.isSet, value.Number.IsSet()) + assert.Equal(t, test.isNil, value.Number.IsNil()) + assert.Equal(t, test.hasValue, value.Number.HasValue()) }) } }) diff --git a/pkg/must/panic_test.go b/pkg/must/panic_test.go index 0ff1ed01..a653427f 100644 --- a/pkg/must/panic_test.go +++ b/pkg/must/panic_test.go @@ -4,8 +4,8 @@ import ( "errors" "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/must" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_Panic(t *testing.T) { @@ -14,8 +14,8 @@ func Test_Panic(t *testing.T) { defer func() { r := recover() - testutil.IsNotNil(t, r) - testutil.ErrorIs(t, err, r.(error)) + assert.NotNil(t, r) + assert.ErrorIs(t, err, r.(error)) }() must.Panic(42, err) @@ -24,6 +24,6 @@ func Test_Panic(t *testing.T) { t.Run("should return the value if no error is given", func(t *testing.T) { value := must.Panic(42, nil) - testutil.Equals(t, 42, value) + assert.Equal(t, 42, value) }) } diff --git a/pkg/ostools/file.go b/pkg/ostools/file.go index 48d39be7..a275ed59 100644 --- a/pkg/ostools/file.go +++ b/pkg/ostools/file.go @@ -1,6 +1,7 @@ package ostools import ( + "errors" "io/fs" "os" "path/filepath" @@ -8,6 +9,8 @@ import ( const defaultPermissions fs.FileMode = 0744 +var ErrTooManyPermissionsGiven = errors.New("too_many_permissions_given") + // Open or create the file to append data only. It also creates intermediate directories as needed. func OpenAppend(name string) (*os.File, error) { if err := MkdirAll(filepath.Dir(name)); err != nil { @@ -25,10 +28,15 @@ func WriteFile(name string, data []byte, perm ...fs.FileMode) error { return err } - filePermissions := defaultPermissions + var filePermissions fs.FileMode - if len(perm) > 0 { + switch len(perm) { + case 0: + filePermissions = defaultPermissions + case 1: filePermissions = perm[0] + default: + return ErrTooManyPermissionsGiven } return os.WriteFile(name, data, filePermissions) diff --git a/pkg/ssh/config_test.go b/pkg/ssh/config_test.go index 20c4fd01..54db22a0 100644 --- a/pkg/ssh/config_test.go +++ b/pkg/ssh/config_test.go @@ -6,11 +6,11 @@ import ( "path/filepath" "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/id" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/ostools" "github.com/YuukanOO/seelf/pkg/ssh" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_FileConfigurator(t *testing.T) { @@ -22,7 +22,7 @@ func Test_FileConfigurator(t *testing.T) { }) if initialConfigContent != "" { - ostools.WriteFile(path, []byte(initialConfigContent)) + _ = ostools.WriteFile(path, []byte(initialConfigContent)) } return ssh.NewFileConfigurator(path), path @@ -31,29 +31,29 @@ func Test_FileConfigurator(t *testing.T) { t.Run("should be able to create a new ssh config if none is found and append the host", func(t *testing.T) { configurator, path := sut("") - testutil.IsNil(t, configurator.Upsert(ssh.Connection{ + assert.Nil(t, configurator.Upsert(ssh.Connection{ Host: "example.com", })) - testutil.FileEquals(t, path, `Host example.com + assert.FileContentEquals(t, `Host example.com StrictHostKeyChecking accept-new -`) +`, path) }) t.Run("should correctly append a host to an existing config file", func(t *testing.T) { configurator, path := sut("Host example.com\nUser root\n") - testutil.IsNil(t, configurator.Upsert(ssh.Connection{ + assert.Nil(t, configurator.Upsert(ssh.Connection{ Host: "somewhere.com", User: monad.Value("user"), Port: monad.Value(2222), })) - testutil.FileEquals(t, path, `Host example.com + assert.FileContentEquals(t, `Host example.com User root Host somewhere.com StrictHostKeyChecking accept-new User user Port 2222 -`) +`, path) }) t.Run("should correctly update an existing host", func(t *testing.T) { @@ -65,18 +65,18 @@ User user Port 2222 `) - testutil.IsNil(t, configurator.Upsert(ssh.Connection{ + assert.Nil(t, configurator.Upsert(ssh.Connection{ Host: "somewhere.com", User: monad.Value("root"), Port: monad.Value(22), })) - testutil.FileEquals(t, path, `Host example.com + assert.FileContentEquals(t, `Host example.com User root Host somewhere.com StrictHostKeyChecking accept-new User root Port 22 -`) +`, path) }) t.Run("should update an host only if the identifier match", func(t *testing.T) { @@ -86,45 +86,45 @@ Host example.com #my-identifier User john `) - testutil.IsNil(t, configurator.Upsert(ssh.Connection{ + assert.Nil(t, configurator.Upsert(ssh.Connection{ Identifier: "my-identifier", Host: "another.com", User: monad.Value("john"), Port: monad.Value(2222), })) - testutil.FileEquals(t, path, `Host example.com + assert.FileContentEquals(t, `Host example.com User root Host another.com #my-identifier StrictHostKeyChecking accept-new User john Port 2222 -`) +`, path) }) t.Run("should write the private key if set", func(t *testing.T) { configurator, path := sut("") expectedKeyPath := filepath.Join(filepath.Dir(path), "privkeyfilename") - testutil.IsNil(t, configurator.Upsert(ssh.Connection{ + assert.Nil(t, configurator.Upsert(ssh.Connection{ Host: "example.com", PrivateKey: monad.Value(ssh.ConnectionKey{ Name: "privkeyfilename", Key: "privkeycontent", }), })) - testutil.FileEquals(t, path, fmt.Sprintf(`Host example.com + assert.FileContentEquals(t, fmt.Sprintf(`Host example.com StrictHostKeyChecking accept-new IdentityFile %s IdentitiesOnly yes -`, expectedKeyPath)) - testutil.FileEquals(t, expectedKeyPath, "privkeycontent") +`, expectedKeyPath), path) + assert.FileContentEquals(t, "privkeycontent", expectedKeyPath) }) t.Run("should remove the old private key if it was set", func(t *testing.T) { configurator, path := sut("") oldKeyPath := filepath.Join(filepath.Dir(path), "oldkeyfilename") newKeyPath := filepath.Join(filepath.Dir(path), "newkeyfilename") - testutil.IsNil(t, configurator.Upsert(ssh.Connection{ + assert.Nil(t, configurator.Upsert(ssh.Connection{ Host: "example.com", PrivateKey: monad.Value(ssh.ConnectionKey{ Name: "oldkeyfilename", @@ -132,26 +132,26 @@ IdentitiesOnly yes }), })) - testutil.IsNil(t, configurator.Upsert(ssh.Connection{ + assert.Nil(t, configurator.Upsert(ssh.Connection{ Host: "example.com", PrivateKey: monad.Value(ssh.ConnectionKey{ Name: "newkeyfilename", Key: "newprivkeycontent", }), })) - testutil.FileEquals(t, path, fmt.Sprintf(`Host example.com + assert.FileContentEquals(t, fmt.Sprintf(`Host example.com StrictHostKeyChecking accept-new IdentityFile %s IdentitiesOnly yes -`, newKeyPath)) - testutil.FileEquals(t, newKeyPath, "newprivkeycontent") - testutil.FileEquals(t, oldKeyPath, "") +`, newKeyPath), path) + assert.FileContentEquals(t, "newprivkeycontent", newKeyPath) + assert.FileContentEquals(t, "", oldKeyPath) }) t.Run("should do nothing if trying to delete an host and no config file exist", func(t *testing.T) { configurator, _ := sut("") - testutil.IsNil(t, configurator.Remove("test")) + assert.Nil(t, configurator.Remove("test")) }) t.Run("should correctly remove an host", func(t *testing.T) { @@ -161,10 +161,10 @@ Host example.com #my-identifier User john `) - testutil.IsNil(t, configurator.Remove("")) - testutil.FileEquals(t, path, `Host example.com #my-identifier + assert.Nil(t, configurator.Remove("")) + assert.FileContentEquals(t, `Host example.com #my-identifier User john -`) +`, path) }) t.Run("should correctly remove an host with a specific identifier", func(t *testing.T) { @@ -174,25 +174,25 @@ Host example.com #my-identifier User john `) - testutil.IsNil(t, configurator.Remove("my-identifier")) - testutil.FileEquals(t, path, `Host example.com + assert.Nil(t, configurator.Remove("my-identifier")) + assert.FileContentEquals(t, `Host example.com User root -`) +`, path) }) t.Run("should remove the private key attached to the host being removed", func(t *testing.T) { configurator, path := sut("") keyPath := filepath.Join(filepath.Dir(path), "privkeyfilename") - configurator.Upsert(ssh.Connection{ + assert.Nil(t, configurator.Upsert(ssh.Connection{ Host: "example.com", PrivateKey: monad.Value(ssh.ConnectionKey{ Name: "privkeyfilename", Key: "privkeycontent", }), - }) + })) - testutil.IsNil(t, configurator.Remove("")) - testutil.FileEquals(t, path, "") - testutil.FileEquals(t, keyPath, "") + assert.Nil(t, configurator.Remove("")) + assert.FileContentEquals(t, "", path) + assert.FileContentEquals(t, "", keyPath) }) } diff --git a/pkg/ssh/host_test.go b/pkg/ssh/host_test.go index 0a3b77f3..b15f9054 100644 --- a/pkg/ssh/host_test.go +++ b/pkg/ssh/host_test.go @@ -3,9 +3,9 @@ package ssh_test import ( "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/must" "github.com/YuukanOO/seelf/pkg/ssh" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_Host(t *testing.T) { @@ -30,18 +30,18 @@ func Test_Host(t *testing.T) { got, err := ssh.ParseHost(tt.value) if !tt.valid { - testutil.ErrorIs(t, ssh.ErrInvalidHost, err) + assert.ErrorIs(t, ssh.ErrInvalidHost, err) return } - testutil.IsNil(t, err) - testutil.Equals(t, tt.value, string(got)) + assert.Nil(t, err) + assert.Equal(t, tt.value, string(got)) }) } }) t.Run("should returns a string representation", func(t *testing.T) { h := must.Panic(ssh.ParseHost("localhost")) - testutil.Equals(t, "localhost", h.String()) + assert.Equal(t, "localhost", h.String()) }) } diff --git a/pkg/ssh/private_key_test.go b/pkg/ssh/private_key_test.go index ca1cac8f..e51444f4 100644 --- a/pkg/ssh/private_key_test.go +++ b/pkg/ssh/private_key_test.go @@ -3,8 +3,8 @@ package ssh_test import ( "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/ssh" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_PrivateKey(t *testing.T) { @@ -38,12 +38,12 @@ wSD0v0RcmkITP1ZR0AAAAYcHF1ZXJuYUBMdWNreUh5ZHJvLmxvY2FsAQID got, err := ssh.ParsePrivateKey(tt.value) if !tt.valid { - testutil.ErrorIs(t, ssh.ErrInvalidSSHKey, err) + assert.ErrorIs(t, ssh.ErrInvalidSSHKey, err) return } - testutil.IsNil(t, err) - testutil.Equals(t, tt.value, string(got)) + assert.Nil(t, err) + assert.Equal(t, tt.value, string(got)) }) } }) diff --git a/pkg/storage/discriminated.go b/pkg/storage/discriminated.go index e782f1cb..8c87197b 100644 --- a/pkg/storage/discriminated.go +++ b/pkg/storage/discriminated.go @@ -29,6 +29,8 @@ func NewDiscriminatedMapper[T any]( } // Register a new concrete type available to the mapper. +// It will panic if a type is already registered with the same discriminator since it +// should never happen. func (m *DiscriminatedMapper[T]) Register(concreteType T, mapper DiscriminatedMapperFunc[T]) { discriminator := m.extractor(concreteType) diff --git a/pkg/storage/discriminated_test.go b/pkg/storage/discriminated_test.go index e74552f8..dc9dc321 100644 --- a/pkg/storage/discriminated_test.go +++ b/pkg/storage/discriminated_test.go @@ -3,8 +3,8 @@ package storage_test import ( "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/storage" - "github.com/YuukanOO/seelf/pkg/testutil" ) type ( @@ -45,18 +45,18 @@ func Test_Discriminated(t *testing.T) { t.Run("should error if the discriminator is not known", func(t *testing.T) { _, err := mapper.From("unknown", "") - testutil.ErrorIs(t, err, storage.ErrCouldNotUnmarshalGivenType) + assert.ErrorIs(t, err, storage.ErrCouldNotUnmarshalGivenType) }) t.Run("should return the correct type", func(t *testing.T) { t1, err := mapper.From("type1", "data1") - testutil.IsNil(t, err) - testutil.Equals(t, type1{"data1"}, t1.(type1)) + assert.Nil(t, err) + assert.Equal(t, type1{"data1"}, t1.(type1)) t2, err := mapper.From("type2", "data2") - testutil.IsNil(t, err) - testutil.Equals(t, type2{"data2"}, t2.(type2)) + assert.Nil(t, err) + assert.Equal(t, type2{"data2"}, t2.(type2)) }) } diff --git a/pkg/storage/secret_string_test.go b/pkg/storage/secret_string_test.go index e5271e59..a899825f 100644 --- a/pkg/storage/secret_string_test.go +++ b/pkg/storage/secret_string_test.go @@ -3,8 +3,8 @@ package storage_test import ( "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/storage" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_SecretString(t *testing.T) { @@ -13,8 +13,8 @@ func Test_SecretString(t *testing.T) { err := s.Scan("test") - testutil.IsNil(t, err) - testutil.Equals(t, "test", s) + assert.Nil(t, err) + assert.Equal(t, "test", s) }) t.Run("should marshal to a json string with the same length as the original string and custom characters", func(t *testing.T) { @@ -23,8 +23,8 @@ func Test_SecretString(t *testing.T) { data, err := s.MarshalJSON() dataStr := string(data) - testutil.IsNil(t, err) - testutil.Equals(t, `"******************"`, dataStr) + assert.Nil(t, err) + assert.Equal(t, `"******************"`, dataStr) }) t.Run("should keep newlines", func(t *testing.T) { @@ -35,7 +35,7 @@ and another one`) data, err := s.MarshalJSON() dataStr := string(data) - testutil.IsNil(t, err) - testutil.Equals(t, `"******************\n**************\n***************"`, dataStr) + assert.Nil(t, err) + assert.Equal(t, `"******************\n**************\n***************"`, dataStr) }) } diff --git a/pkg/storage/sqlite/builder/builder_test.go b/pkg/storage/sqlite/builder/builder_test.go index bcf59483..b8f424cb 100644 --- a/pkg/storage/sqlite/builder/builder_test.go +++ b/pkg/storage/sqlite/builder/builder_test.go @@ -3,9 +3,9 @@ package builder_test import ( "testing" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/monad" "github.com/YuukanOO/seelf/pkg/storage/sqlite/builder" - "github.com/YuukanOO/seelf/pkg/testutil" ) func Test_Builder(t *testing.T) { @@ -13,7 +13,7 @@ func Test_Builder(t *testing.T) { q := builder. Query[any]("SELECT id, name FROM some_table WHERE name = ?", "john") - testutil.Equals(t, "SELECT id, name FROM some_table WHERE name = ?", q.String()) + assert.Equal(t, "SELECT id, name FROM some_table WHERE name = ?", q.String()) }) t.Run("should handle statements", func(t *testing.T) { @@ -34,7 +34,7 @@ func Test_Builder(t *testing.T) { ). F("ORDER BY name") - testutil.Equals(t, "SELECT id, name FROM some_table WHERE name = ? AND id = ? AND age IN (?,?) AND TRUE ORDER BY name", q.String()) + assert.Equal(t, "SELECT id, name FROM some_table WHERE name = ? AND id = ? AND age IN (?,?) AND TRUE ORDER BY name", q.String()) }) t.Run("should handle insert statements", func(t *testing.T) { @@ -44,7 +44,7 @@ func Test_Builder(t *testing.T) { "id": 1, }) - testutil.Match(t, "INSERT INTO some_table \\((,?(age|name|id)){3}\\) VALUES \\(\\?,\\?,\\?\\)", q.String()) + assert.Match(t, "INSERT INTO some_table \\((,?(age|name|id)){3}\\) VALUES \\(\\?,\\?,\\?\\)", q.String()) }) t.Run("should handle update statements", func(t *testing.T) { @@ -53,6 +53,6 @@ func Test_Builder(t *testing.T) { "age": 21, }).F("WHERE id = ?", 1) - testutil.Match(t, "UPDATE some_table SET (,?(age|name) = \\?){2} WHERE id = \\?", q.String()) + assert.Match(t, "UPDATE some_table SET (,?(age|name) = \\?){2} WHERE id = \\?", q.String()) }) } diff --git a/pkg/testutil/assertion.go b/pkg/testutil/assertion.go deleted file mode 100644 index 5b3ce920..00000000 --- a/pkg/testutil/assertion.go +++ /dev/null @@ -1,130 +0,0 @@ -// Package testutil exposes assert utilities used in the project to make things -// simpler to read. -package testutil - -import ( - "errors" - "os" - "reflect" - "regexp" - "strings" - "testing" - "unicode/utf8" - - "github.com/YuukanOO/seelf/pkg/event" -) - -func Equals[T comparable](t testing.TB, expected, actual T) { - if expected != actual { - expectationVersusReality(t, "should have been equals", expected, actual) - } -} - -func NotEquals[T comparable](t testing.TB, expected, actual T) { - if expected == actual { - expectationVersusReality(t, "should not have been equals", expected, actual) - } -} - -func DeepEquals[T any](t testing.TB, expected, actual T) { - if !reflect.DeepEqual(expected, actual) { - expectationVersusReality(t, "should have been deeply equals", expected, actual) - } -} - -func IsTrue[T ~bool](t testing.TB, expr T) { - Equals(t, true, expr) -} - -func IsFalse[T ~bool](t testing.TB, expr T) { - Equals(t, false, expr) -} - -func IsNil(t testing.TB, expr any) { - if expr != nil { - expectationVersusReality(t, "should have been nil", nil, expr) - } -} - -func IsNotNil(t testing.TB, expr any) { - if expr == nil { - expectationVersusReality(t, "should have been not nil", "nothing but ", expr) - } -} - -func HasLength[T any](t testing.TB, arr []T, length int) { - actual := len(arr) - if actual != length { - expectationVersusReality(t, "should have correct size", length, actual) - } -} - -func HasNChars[T ~string](t testing.TB, expected int, value T) { - actual := utf8.RuneCountInString(string(value)) - - if actual != expected { - expectationVersusReality(t, "should have correct number of characters", expected, actual) - } -} - -func Contains(t testing.TB, expected string, value string) { - if !strings.Contains(value, expected) { - expectationVersusReality(t, "should contains the string", expected, value) - } -} - -func Match(t testing.TB, re string, value string) { - if !regexp.MustCompile(re).MatchString(value) { - expectationVersusReality(t, "should match", re, value) - } -} - -func ErrorIs(t testing.TB, expected, actual error) { - if !errors.Is(actual, expected) { - expectationVersusReality(t, "errors should have match", expected, actual) - } -} - -func HasNEvents(t testing.TB, source event.Source, expected int) { - actual := len(event.Unwrap(source)) - - if actual != expected { - expectationVersusReality(t, "should have correct number of events", expected, actual) - } -} - -func EventIs[T event.Event](t testing.TB, source event.Source, index int) (result T) { - events := event.Unwrap(source) - - if index >= len(events) { - expectationVersusReality(t, "could not find an event at given index", index, nil) - return result - } - - result, ok := events[index].(T) - - if !ok { - expectationVersusReality(t, "wrong event type", events[index], result) - return result - } - - return result -} - -func FileEquals(t testing.TB, path, expected string) { - data, _ := os.ReadFile(path) - str := string(data) - - if str != expected { - expectationVersusReality(t, "file content should have been equals", expected, str) - } -} - -func expectationVersusReality(t testing.TB, message string, expected, actual any) { - t.Fatalf(`%s - expected: -%v - - got: -%v`, message, expected, actual) -} diff --git a/pkg/testutil/assertion_test.go b/pkg/testutil/assertion_test.go deleted file mode 100644 index fbddedaa..00000000 --- a/pkg/testutil/assertion_test.go +++ /dev/null @@ -1,432 +0,0 @@ -package testutil_test - -import ( - "errors" - "fmt" - "os" - "testing" - - "github.com/YuukanOO/seelf/pkg/bus" - "github.com/YuukanOO/seelf/pkg/event" - "github.com/YuukanOO/seelf/pkg/testutil" -) - -type testMock struct { - testing.TB - hasFailed bool -} - -func (t *testMock) Fatalf(format string, args ...any) { - // TODO: must test the error message too - t.hasFailed = true -} - -func Test_Equals(t *testing.T) { - tests := []struct { - expected bool - actual bool - shouldFail bool - }{ - {true, false, true}, - {true, true, false}, - {false, false, false}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) { - mock := new(testMock) - - testutil.Equals(mock, test.expected, test.actual) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_NotEquals(t *testing.T) { - tests := []struct { - expected bool - actual bool - shouldFail bool - }{ - {true, true, true}, - {false, true, false}, - {false, false, true}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) { - mock := new(testMock) - - testutil.NotEquals(mock, test.expected, test.actual) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_DeepEquals(t *testing.T) { - tests := []struct { - expected []bool - actual []bool - shouldFail bool - }{ - {[]bool{true, true}, []bool{false, true}, true}, - {[]bool{true, true}, []bool{true, true}, false}, - {[]bool{false, false}, []bool{false, true}, true}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) { - mock := new(testMock) - - testutil.DeepEquals(mock, test.expected, test.actual) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_IsTrue(t *testing.T) { - tests := []struct { - actual bool - shouldFail bool - }{ - {true, false}, - {false, true}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v", test.actual), func(t *testing.T) { - mock := new(testMock) - - testutil.IsTrue(mock, test.actual) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_IsFalse(t *testing.T) { - tests := []struct { - actual bool - shouldFail bool - }{ - {true, true}, - {false, false}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v", test.actual), func(t *testing.T) { - mock := new(testMock) - - testutil.IsFalse(mock, test.actual) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_IsNil(t *testing.T) { - tests := []struct { - actual any - shouldFail bool - }{ - {true, true}, - {nil, false}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v", test.actual), func(t *testing.T) { - mock := new(testMock) - - testutil.IsNil(mock, test.actual) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_IsNotNil(t *testing.T) { - tests := []struct { - actual any - shouldFail bool - }{ - {true, false}, - {nil, true}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v", test.actual), func(t *testing.T) { - mock := new(testMock) - - testutil.IsNotNil(mock, test.actual) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_HasLength(t *testing.T) { - tests := []struct { - expected int - actual []int - shouldFail bool - }{ - {1, []int{1, 2}, true}, - {2, []int{1, 2}, false}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) { - mock := new(testMock) - - testutil.HasLength(mock, test.actual, test.expected) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_HasNChars(t *testing.T) { - tests := []struct { - expected int - actual string - shouldFail bool - }{ - {5, "a long string", true}, - {2, "hi", false}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) { - mock := new(testMock) - - testutil.HasNChars(mock, test.expected, test.actual) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_Contains(t *testing.T) { - tests := []struct { - value string - search string - shouldFail bool - }{ - {"validation failed", "error", true}, - {"validation failed", "failed", false}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v %v", test.value, test.search), func(t *testing.T) { - mock := new(testMock) - - testutil.Contains(mock, test.search, test.value) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_Match(t *testing.T) { - tests := []struct { - re string - value string - shouldFail bool - }{ - {"abc", "error", true}, - {"abc", "abc", false}, - {"abc?", "ab", false}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v %v", test.value, test.re), func(t *testing.T) { - mock := new(testMock) - - testutil.Match(mock, test.re, test.value) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_ErrorIs(t *testing.T) { - err := errors.New("some error") - - tests := []struct { - expected error - actual error - shouldFail bool - }{ - {err, errors.New("another one"), true}, - {err, err, false}, - {err, fmt.Errorf("with wrapped error %w", err), false}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v %v", test.expected, test.actual), func(t *testing.T) { - mock := new(testMock) - - testutil.ErrorIs(mock, test.expected, test.actual) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -type ( - domainEntity struct { - event.Emitter - } - - eventA struct { - bus.Notification - msg string - } - - eventB struct { - bus.Notification - number int - } -) - -func (eventA) Name_() string { return "eventA" } -func (eventB) Name_() string { return "eventB" } - -func Test_EventIs(t *testing.T) { - var entity domainEntity - - entity = entity.apply(eventA{msg: "test"}).apply(eventB{number: 42}) - - t.Run("should be able to retrieve an event if it exists", func(t *testing.T) { - evt := testutil.EventIs[eventA](t, &entity, 0) - evt2 := testutil.EventIs[eventB](t, &entity, 1) - - testutil.Equals(t, "test", evt.msg) - testutil.Equals(t, 42, evt2.number) - }) - - t.Run("should fail if no events exists at all", func(t *testing.T) { - mock := new(testMock) - - testutil.EventIs[eventA](mock, &domainEntity{}, 0) - - if !mock.hasFailed { - t.Fail() - } - }) - - t.Run("should fail if trying to access a not in range index", func(t *testing.T) { - mock := new(testMock) - - testutil.EventIs[eventA](mock, &entity, 2) - - if !mock.hasFailed { - t.Fail() - } - }) - - t.Run("should fail if type does not match", func(t *testing.T) { - mock := new(testMock) - testutil.EventIs[eventB](mock, &entity, 0) - - if !mock.hasFailed { - t.Fail() - } - }) -} - -func Test_HasNEvents(t *testing.T) { - var entity domainEntity - - entity = entity.apply(eventA{msg: "test"}).apply(eventB{number: 42}) - - tests := []struct { - expected int - shouldFail bool - }{ - {1, true}, - {2, false}, - {4, true}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v", test.expected), func(t *testing.T) { - mock := new(testMock) - - testutil.HasNEvents(mock, &entity, test.expected) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - } -} - -func Test_FileEquals(t *testing.T) { - path := "testfile" - - t.Cleanup(func() { - os.RemoveAll(path) - }) - - tests := []struct { - actual string - expected string - shouldFail bool - }{ - {"test", "test", false}, - {"test", "test2", true}, - {"", "test", true}, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%v %v", test.actual, test.expected), func(t *testing.T) { - if test.actual != "" { - os.WriteFile(path, []byte(test.actual), 0644) - } else { - os.RemoveAll(path) - } - - mock := new(testMock) - - testutil.FileEquals(mock, path, test.expected) - - if mock.hasFailed != test.shouldFail { - t.Fail() - } - }) - - } -} - -func (d domainEntity) apply(e event.Event) domainEntity { - event.Store(&d, e) - return d -} diff --git a/pkg/types/is_test.go b/pkg/types/is_test.go index 620cf0c6..7dd76e99 100644 --- a/pkg/types/is_test.go +++ b/pkg/types/is_test.go @@ -3,7 +3,7 @@ package types_test import ( "testing" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/types" ) @@ -19,7 +19,7 @@ func Test_Is(t *testing.T) { t2 any = type2{} ) - testutil.IsTrue(t, types.Is[type1](t1)) - testutil.IsFalse(t, types.Is[type1](t2)) + assert.True(t, types.Is[type1](t1)) + assert.False(t, types.Is[type1](t2)) }) } diff --git a/pkg/validate/numbers/numbers_test.go b/pkg/validate/numbers/numbers_test.go index 5ee92865..2e9098f0 100644 --- a/pkg/validate/numbers/numbers_test.go +++ b/pkg/validate/numbers/numbers_test.go @@ -3,18 +3,18 @@ package numbers_test import ( "testing" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/validate/numbers" ) func Test_Min(t *testing.T) { t.Run("should fail on value lesser than the required min", func(t *testing.T) { - testutil.ErrorIs(t, numbers.ErrMin, numbers.Min(3)(2)) - testutil.ErrorIs(t, numbers.ErrMin, numbers.Min(3)(1)) + assert.ErrorIs(t, numbers.ErrMin, numbers.Min(3)(2)) + assert.ErrorIs(t, numbers.ErrMin, numbers.Min(3)(1)) }) t.Run("should succeed on value greater then the required min", func(t *testing.T) { - testutil.IsNil(t, numbers.Min(3)(4)) - testutil.IsNil(t, numbers.Min(3)(3)) + assert.Nil(t, numbers.Min(3)(4)) + assert.Nil(t, numbers.Min(3)(3)) }) } diff --git a/pkg/validate/strings/strings_test.go b/pkg/validate/strings/strings_test.go index 380cad42..12cb6cb1 100644 --- a/pkg/validate/strings/strings_test.go +++ b/pkg/validate/strings/strings_test.go @@ -4,18 +4,18 @@ import ( "regexp" "testing" - "github.com/YuukanOO/seelf/pkg/testutil" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/validate/strings" ) func Test_Required(t *testing.T) { t.Run("should fail on empty or whitespaced strings", func(t *testing.T) { - testutil.ErrorIs(t, strings.ErrRequired, strings.Required("")) - testutil.ErrorIs(t, strings.ErrRequired, strings.Required(" ")) + assert.ErrorIs(t, strings.ErrRequired, strings.Required("")) + assert.ErrorIs(t, strings.ErrRequired, strings.Required(" ")) }) t.Run("should succeed on non-empty strings", func(t *testing.T) { - testutil.IsNil(t, strings.Required("should be good")) + assert.Nil(t, strings.Required("should be good")) }) } @@ -23,33 +23,33 @@ func Test_Match(t *testing.T) { reUrlFormat := regexp.MustCompile("^https?://.+") t.Run("should fail on non matching strings", func(t *testing.T) { - testutil.ErrorIs(t, strings.ErrFormat, strings.Match(reUrlFormat)("some string")) - testutil.ErrorIs(t, strings.ErrFormat, strings.Match(reUrlFormat)("http://")) + assert.ErrorIs(t, strings.ErrFormat, strings.Match(reUrlFormat)("some string")) + assert.ErrorIs(t, strings.ErrFormat, strings.Match(reUrlFormat)("http://")) }) t.Run("should succeed when matching", func(t *testing.T) { - testutil.IsNil(t, strings.Match(reUrlFormat)("http://docker.localhost")) + assert.Nil(t, strings.Match(reUrlFormat)("http://docker.localhost")) }) } func Test_Min(t *testing.T) { t.Run("should fail on strings with less characters than the given length", func(t *testing.T) { - testutil.ErrorIs(t, strings.ErrMinLength, strings.Min(5)("")) - testutil.ErrorIs(t, strings.ErrMinLength, strings.Min(5)("test")) + assert.ErrorIs(t, strings.ErrMinLength, strings.Min(5)("")) + assert.ErrorIs(t, strings.ErrMinLength, strings.Min(5)("test")) }) t.Run("should succeed when enough characters are given", func(t *testing.T) { - testutil.IsNil(t, strings.Min(5)("should be good")) + assert.Nil(t, strings.Min(5)("should be good")) }) } func Test_Max(t *testing.T) { t.Run("should fail on strings with more characters than the given length", func(t *testing.T) { - testutil.ErrorIs(t, strings.ErrMaxLength, strings.Max(5)("should not be good")) - testutil.ErrorIs(t, strings.ErrMaxLength, strings.Max(5)("errorr")) + assert.ErrorIs(t, strings.ErrMaxLength, strings.Max(5)("should not be good")) + assert.ErrorIs(t, strings.ErrMaxLength, strings.Max(5)("errorr")) }) t.Run("should succeed when less characters than length are given", func(t *testing.T) { - testutil.IsNil(t, strings.Max(5)("yeah!")) + assert.Nil(t, strings.Max(5)("yeah!")) }) } diff --git a/pkg/validate/validate_test.go b/pkg/validate/validate_test.go index b49404a6..35afcc34 100644 --- a/pkg/validate/validate_test.go +++ b/pkg/validate/validate_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/YuukanOO/seelf/pkg/apperr" + "github.com/YuukanOO/seelf/pkg/assert" "github.com/YuukanOO/seelf/pkg/monad" - "github.com/YuukanOO/seelf/pkg/testutil" "github.com/YuukanOO/seelf/pkg/validate" ) @@ -30,17 +30,17 @@ func alwaysFail(value string) error { func Test_Field(t *testing.T) { t.Run("call every validators", func(t *testing.T) { err := validate.Field("", required, alwaysFail) - testutil.ErrorIs(t, errRequired, err) + assert.ErrorIs(t, errRequired, err) }) t.Run("returns nil when validation pass successfully", func(t *testing.T) { err := validate.Field("something", required) - testutil.IsNil(t, err) + assert.Nil(t, err) }) t.Run("returns the validator error", func(t *testing.T) { err := validate.Field("something", required, alwaysFail) - testutil.ErrorIs(t, errAlwaysFail, err) + assert.ErrorIs(t, errAlwaysFail, err) }) } @@ -59,16 +59,16 @@ func Test_Value(t *testing.T) { var target objectValue err := validate.Value("", &target, objectValueFactory) - testutil.ErrorIs(t, errRequired, err) - testutil.Equals(t, "", target) + assert.ErrorIs(t, errRequired, err) + assert.Equal(t, "", target) }) t.Run("returns nil error and assign the target upon success", func(t *testing.T) { var target objectValue err := validate.Value("something", &target, objectValueFactory) - testutil.IsNil(t, err) - testutil.Equals(t, "something", target) + assert.Nil(t, err) + assert.Equal(t, "something", target) }) } @@ -79,15 +79,13 @@ func Test_Struct(t *testing.T) { "lastName": validate.Field("doe", required, alwaysFail), }) - testutil.Contains(t, "validation_failed:", err.Error()) - testutil.Contains(t, "firstName: required", err.Error()) - testutil.Contains(t, "lastName: always fail", err.Error()) - testutil.ErrorIs(t, validate.ErrValidationFailed, err) - validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.Equals(t, 2, len(validationErr)) - testutil.ErrorIs(t, errRequired, validationErr["firstName"]) - testutil.ErrorIs(t, errAlwaysFail, validationErr["lastName"]) + assert.Match(t, "validation_failed:", err.Error()) + assert.Match(t, "firstName: required", err.Error()) + assert.Match(t, "lastName: always fail", err.Error()) + assert.ValidationError(t, validate.FieldErrors{ + "firstName": errRequired, + "lastName": errAlwaysFail, + }, err) }) t.Run("merge nested validation errors", func(t *testing.T) { @@ -103,12 +101,12 @@ func Test_Struct(t *testing.T) { }) validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.Equals(t, 4, len(validationErr)) - testutil.ErrorIs(t, errRequired, validationErr["firstName"]) - testutil.ErrorIs(t, errAlwaysFail, validationErr["lastName"]) - testutil.ErrorIs(t, errRequired, validationErr["nested.firstName"]) - testutil.ErrorIs(t, errRequired, validationErr["nested.nested.firstName"]) + assert.True(t, ok) + assert.Equal(t, 4, len(validationErr)) + assert.ErrorIs(t, errRequired, validationErr["firstName"]) + assert.ErrorIs(t, errAlwaysFail, validationErr["lastName"]) + assert.ErrorIs(t, errRequired, validationErr["nested.firstName"]) + assert.ErrorIs(t, errRequired, validationErr["nested.nested.firstName"]) }) t.Run("returns nil if no error exists", func(t *testing.T) { @@ -117,7 +115,7 @@ func Test_Struct(t *testing.T) { "lastName": validate.Field("doe", required), }) - testutil.IsNil(t, err) + assert.Nil(t, err) }) } @@ -128,12 +126,12 @@ func Test_If(t *testing.T) { "lastName": validate.If(true, func() error { return validate.Field("", required) }), }) - testutil.Equals(t, `validation_failed: + assert.Equal(t, `validation_failed: lastName: required`, err.Error()) validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.Equals(t, 1, len(validationErr)) - testutil.ErrorIs(t, errRequired, validationErr["lastName"]) + assert.True(t, ok) + assert.Equal(t, 1, len(validationErr)) + assert.ErrorIs(t, errRequired, validationErr["lastName"]) }) } @@ -145,7 +143,7 @@ func Test_Maybe(t *testing.T) { return validate.Field(val, required) }) - testutil.IsNil(t, err) + assert.Nil(t, err) }) t.Run("executes the function if the monad is set", func(t *testing.T) { @@ -155,7 +153,7 @@ func Test_Maybe(t *testing.T) { return validate.Field(val, required) }) - testutil.ErrorIs(t, errRequired, err) + assert.ErrorIs(t, errRequired, err) }) } @@ -167,7 +165,7 @@ func Test_Patch(t *testing.T) { return validate.Field(val, required) }) - testutil.IsNil(t, err) + assert.Nil(t, err) }) t.Run("executes the function if the patch is set", func(t *testing.T) { @@ -177,7 +175,7 @@ func Test_Patch(t *testing.T) { return validate.Field(val, required) }) - testutil.ErrorIs(t, errRequired, err) + assert.ErrorIs(t, errRequired, err) }) } @@ -185,26 +183,26 @@ func Test_Wrap(t *testing.T) { t.Run("returns the error if it's not an application level error", func(t *testing.T) { infrastructureErr := errors.New("an infrastructure error") - testutil.IsTrue(t, validate.Wrap(infrastructureErr, "one", "two") == infrastructureErr) - testutil.IsTrue(t, validate.Wrap(nil, "one", "two") == nil) + assert.True(t, validate.Wrap(infrastructureErr, "one", "two") == infrastructureErr) + assert.True(t, validate.Wrap(nil, "one", "two") == nil) }) t.Run("returns nil if no err is given", func(t *testing.T) { - testutil.IsNil(t, validate.Wrap(nil, "one", "two")) + assert.Nil(t, validate.Wrap(nil, "one", "two")) }) t.Run("wrap the application error for the specified fields", func(t *testing.T) { appErr := apperr.New("application level error") err := validate.Wrap(appErr, "one", "two") - testutil.ErrorIs(t, validate.ErrValidationFailed, err) + assert.ErrorIs(t, validate.ErrValidationFailed, err) validationErr, ok := apperr.As[validate.FieldErrors](err) fmt.Println(validationErr.Error()) - testutil.IsTrue(t, ok) - testutil.Equals(t, 2, len(validationErr)) - testutil.ErrorIs(t, appErr, validationErr["one"]) - testutil.ErrorIs(t, appErr, validationErr["two"]) + assert.True(t, ok) + assert.Equal(t, 2, len(validationErr)) + assert.ErrorIs(t, appErr, validationErr["one"]) + assert.ErrorIs(t, appErr, validationErr["two"]) }) t.Run("flatten nested validation errors", func(t *testing.T) { @@ -215,14 +213,14 @@ func Test_Wrap(t *testing.T) { err := validate.Wrap(appErr, "one", "two") - testutil.ErrorIs(t, validate.ErrValidationFailed, err) + assert.ErrorIs(t, validate.ErrValidationFailed, err) validationErr, ok := apperr.As[validate.FieldErrors](err) - testutil.IsTrue(t, ok) - testutil.Equals(t, 4, len(validationErr)) - testutil.ErrorIs(t, errRequired, validationErr["one.firstName"]) - testutil.ErrorIs(t, errAlwaysFail, validationErr["one.lastName"]) - testutil.ErrorIs(t, errRequired, validationErr["two.firstName"]) - testutil.ErrorIs(t, errAlwaysFail, validationErr["two.lastName"]) + assert.True(t, ok) + assert.Equal(t, 4, len(validationErr)) + assert.ErrorIs(t, errRequired, validationErr["one.firstName"]) + assert.ErrorIs(t, errAlwaysFail, validationErr["one.lastName"]) + assert.ErrorIs(t, errRequired, validationErr["two.firstName"]) + assert.ErrorIs(t, errAlwaysFail, validationErr["two.lastName"]) }) } @@ -239,7 +237,7 @@ func Test_FieldErrors(t *testing.T) { "4": nil, }.Flatten() - testutil.DeepEquals(t, validate.FieldErrors{ + assert.DeepEqual(t, validate.FieldErrors{ "1": errRequired, "2.1": errAlwaysFail, "3.1": errRequired,