diff --git a/cmd/configuration.go b/cmd/configuration.go index e210f246..9c05cf03 100644 --- a/cmd/configuration.go +++ b/cmd/configuration.go @@ -141,9 +141,9 @@ func (c configuration) RunnersPollInterval() time.Duration { return c.pol func (c configuration) RunnersDeploymentCount() int { return c.Runners.Deployment } func (c configuration) IsSecure() bool { - // If secure has been explicitly set, returns it - if c.Http.Secure.HasValue() { - return c.Http.Secure.MustGet() + // If secure has been explicitly isSet, returns it + if secure, isSet := c.Http.Secure.TryGet(); isSet { + return secure } // Else, fallback to the domain SSL value diff --git a/cmd/serve/deployments.go b/cmd/serve/deployments.go index 6ea9e828..5eeb6d97 100644 --- a/cmd/serve/deployments.go +++ b/cmd/serve/deployments.go @@ -31,10 +31,10 @@ func (s *server) queueDeploymentHandler() gin.HandlerFunc { ) // Resolve the payload data. - if body.Git.HasValue() { - payload = body.Git.MustGet() - } else if body.Raw.HasValue() { - payload = body.Raw.MustGet() + if gitBody, isSet := body.Git.TryGet(); isSet { + payload = gitBody + } else if rawBody, isSet := body.Raw.TryGet(); isSet { + payload = rawBody } else if body.Archive != nil { payload = body.Archive } diff --git a/cmd/serve/front/src/lib/localization/fr.ts b/cmd/serve/front/src/lib/localization/fr.ts index ca010190..20b862df 100644 --- a/cmd/serve/front/src/lib/localization/fr.ts +++ b/cmd/serve/front/src/lib/localization/fr.ts @@ -53,7 +53,7 @@ Cette action est IRRÉVERSIBLE et supprimera TOUTES LES DONNÉES associées : co `Aucune variable pour l'environnement ${name}.`, 'app.cleanup_requested': 'Suppression demandée', 'app.cleanup_requested.description': function (date: DateValue) { - return `La suppression de l'application a été demandé le ${this.date( + return `La suppression de l'application a été demandée le ${this.date( date )} et sera traitée sous peu.`; }, diff --git a/internal/auth/app/command/update_user.go b/internal/auth/app/command/update_user.go index 0ff2c4e7..6967539e 100644 --- a/internal/auth/app/command/update_user.go +++ b/internal/auth/app/command/update_user.go @@ -51,8 +51,8 @@ func UpdateUser( user.HasEmail(uniqueEmail) } - if cmd.Password.HasValue() { - hash, err := hasher.Hash(cmd.Password.MustGet()) + if newPassword, isSet := cmd.Password.TryGet(); isSet { + hash, err := hasher.Hash(newPassword) if err != nil { return err diff --git a/internal/deployment/app/command/create_app.go b/internal/deployment/app/command/create_app.go index 5e85b2d8..65cc0778 100644 --- a/internal/deployment/app/command/create_app.go +++ b/internal/deployment/app/command/create_app.go @@ -60,12 +60,11 @@ func CreateApp( app := domain.NewApp(uniqueName, auth.CurrentUser(ctx).MustGet()) - if cmd.VCS.HasValue() { + if cmdVCS, isSet := cmd.VCS.TryGet(); isSet { vcs := domain.NewVCSConfig(url) - cmdVCS := cmd.VCS.MustGet() - if cmdVCS.Token.HasValue() { - vcs = vcs.Authenticated(cmdVCS.Token.MustGet()) + if token, isSet := cmdVCS.Token.TryGet(); isSet { + vcs = vcs.Authenticated(token) } app.UseVersionControl(vcs) diff --git a/internal/deployment/app/command/update_app.go b/internal/deployment/app/command/update_app.go index 9a6e3d8a..e0000ad2 100644 --- a/internal/deployment/app/command/update_app.go +++ b/internal/deployment/app/command/update_app.go @@ -54,31 +54,30 @@ func UpdateApp( return err } - if cmd.VCS.IsSet() { - if cmd.VCS.IsNil() { - app.RemoveVersionControl() - } else { - vcsPatch := cmd.VCS.MustGet() - - if !app.VCS().HasValue() && !vcsPatch.Url.HasValue() { + if vcsPatch, isSet := cmd.VCS.TryGet(); isSet { + if vcsUpdate, hasValue := vcsPatch.TryGet(); hasValue { + // No VCS configured on the app and no url given + if !app.VCS().HasValue() && !vcsUpdate.Url.HasValue() { return domain.ErrVCSNotConfigured } vcs := app.VCS().Get(domain.NewVCSConfig(url)) - if vcsPatch.Url.HasValue() { + if vcsUpdate.Url.HasValue() { vcs = vcs.WithUrl(url) } - if vcsPatch.Token.IsSet() { - if vcsPatch.Token.IsNil() { - vcs = vcs.Public() + if tokenPatch, isSet := vcsUpdate.Token.TryGet(); isSet { + if token, hasValue := tokenPatch.TryGet(); hasValue { + vcs = vcs.Authenticated(token) } else { - vcs = vcs.Authenticated(vcsPatch.Token.MustGet()) + vcs = vcs.Public() } } app.UseVersionControl(vcs) + } else { + app.RemoveVersionControl() } } diff --git a/internal/deployment/domain/app.go b/internal/deployment/domain/app.go index 5b7c3ce6..8dd127c1 100644 --- a/internal/deployment/domain/app.go +++ b/internal/deployment/domain/app.go @@ -122,18 +122,18 @@ func AppFrom(scanner storage.Scanner) (a App, err error) { a.created = shared.ActionFrom(createdBy, createdAt) - if cleanupRequestedAt.HasValue() { + if requestedAt, isSet := cleanupRequestedAt.TryGet(); isSet { a.cleanupRequested = a.cleanupRequested.WithValue( - shared.ActionFrom(domain.UserID(cleanupRequestedBy.MustGet()), cleanupRequestedAt.MustGet()), + shared.ActionFrom(domain.UserID(cleanupRequestedBy.MustGet()), requestedAt), ) } // vcs url has been set, reconstitute the vcs config - if url.HasValue() { - vcs := NewVCSConfig(url.MustGet()) + if u, isSet := url.TryGet(); isSet { + vcs := NewVCSConfig(u) - if token.HasValue() { - vcs = vcs.Authenticated(token.MustGet()) + if tok, isSet := token.TryGet(); isSet { + vcs = vcs.Authenticated(tok) } a.vcs = a.vcs.WithValue(vcs) @@ -144,7 +144,7 @@ func AppFrom(scanner storage.Scanner) (a App, err error) { // Sets an app version control configuration. func (a *App) UseVersionControl(config VCSConfig) { - if a.vcs.HasValue() && config.Equals(a.vcs.MustGet()) { + if existing, isSet := a.vcs.TryGet(); isSet && config.Equals(existing) { return } @@ -167,7 +167,7 @@ func (a *App) RemoveVersionControl() { // Store environement variables per env and per services for this application. func (a *App) HasEnvironmentVariables(vars EnvironmentsEnv) { - if a.env.HasValue() && vars.Equals(a.env.MustGet()) { + if existing, isSet := a.env.TryGet(); isSet && vars.Equals(existing) { return } @@ -223,11 +223,13 @@ func (a App) VCS() monad.Maybe[VCSConfig] { return a.vcs } // Retrieve environments variables per service for the given deployment environment func (a App) envFor(e Environment) (m monad.Maybe[ServicesEnv]) { - if !a.env.HasValue() { + env, isSet := a.env.TryGet() + + if !isSet { return m } - vars, exists := a.env.MustGet()[e] + vars, exists := env[e] if !exists { return m diff --git a/internal/deployment/domain/config.go b/internal/deployment/domain/config.go index b7286718..9d5c4f7a 100644 --- a/internal/deployment/domain/config.go +++ b/internal/deployment/domain/config.go @@ -31,11 +31,13 @@ func (c Config) Env() monad.Maybe[ServicesEnv] { return c.env } // FIXME: If I w // 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 Config) EnvironmentVariablesFor(service string) (m monad.Maybe[EnvVars]) { - if !c.env.HasValue() { + env, isSet := c.env.TryGet() + + if !isSet { return m } - vars, exists := c.env.MustGet()[service] + vars, exists := env[service] if !exists { return m diff --git a/internal/deployment/infra/backend/docker/docker.go b/internal/deployment/infra/backend/docker/docker.go index 1168855d..8eef0fd3 100644 --- a/internal/deployment/infra/backend/docker/docker.go +++ b/internal/deployment/infra/backend/docker/docker.go @@ -436,8 +436,7 @@ func (d *docker) generateProject(depl domain.Deployment, dir string, logger doma // Attach environment variables if any servicesEnv := config.EnvironmentVariablesFor(deployedService.Name()) - if servicesEnv.HasValue() { - vars := servicesEnv.MustGet() + if vars, hasVars := servicesEnv.TryGet(); hasVars { envNames := make([]string, 0, len(vars)) for name, value := range vars { diff --git a/internal/deployment/infra/source/git/source.go b/internal/deployment/infra/source/git/source.go index 98728a66..40b6aaa5 100644 --- a/internal/deployment/infra/source/git/source.go +++ b/internal/deployment/infra/source/git/source.go @@ -66,12 +66,14 @@ func (s *service) Prepare(app domain.App, payload any) (domain.SourceData, error return nil, err } - if !app.VCS().HasValue() { + vcs, hasVCS := app.VCS().TryGet() + + if !hasVCS { return nil, domain.ErrVCSNotConfigured } // Retrieve the latest commit to make sure the branch exists - latestCommit, err := getLatestBranchCommit(app.VCS().MustGet(), req.Branch) + latestCommit, err := getLatestBranchCommit(vcs, req.Branch) if err != nil { return nil, validation.WrapIfAppErr(err, "branch") @@ -89,26 +91,26 @@ func (s *service) Fetch(ctx context.Context, dir string, logger domain.Deploymen return ErrAppRetrievedFailed } + vcs, hasVCS := app.VCS().TryGet() + // Could happen if the app vcs config has been removed since the deployment has been queued - if !app.VCS().HasValue() { + if !hasVCS { return domain.ErrVCSNotConfigured } - config := app.VCS().MustGet() - data, ok := depl.Source().(Data) if !ok { return domain.ErrInvalidSourcePayload } - logger.Stepf("cloning branch %s at %s from %s using token: %t", data.Branch, data.Hash, config.Url(), config.Token().HasValue()) + logger.Stepf("cloning branch %s at %s from %s using token: %t", data.Branch, data.Hash, vcs.Url(), vcs.Token().HasValue()) r, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ - Auth: getAuthMethod(config), + Auth: getAuthMethod(vcs), SingleBranch: true, ReferenceName: plumbing.NewBranchReferenceName(data.Branch), - URL: config.Url().String(), + URL: vcs.Url().String(), Progress: logger, }) @@ -143,10 +145,10 @@ func (s *service) Fetch(ctx context.Context, dir string, logger domain.Deploymen } func getAuthMethod(vcs domain.VCSConfig) transport.AuthMethod { - if vcs.Token().HasValue() { + if token, isSet := vcs.Token().TryGet(); isSet { return &http.BasicAuth{ Username: basicAuthUser, - Password: vcs.Token().MustGet(), + Password: token, } } diff --git a/internal/deployment/infra/sqlite/gateway.go b/internal/deployment/infra/sqlite/gateway.go index 0cc523e9..4a3adf2f 100644 --- a/internal/deployment/infra/sqlite/gateway.go +++ b/internal/deployment/infra/sqlite/gateway.go @@ -192,9 +192,9 @@ func appDetailDataMapper(s storage.Scanner) (a query.AppDetail, err error) { a.Environments = make(map[string]query.Deployment) - if url.HasValue() { + if u, isSet := url.TryGet(); isSet { a.VCS = a.VCS.WithValue(query.VCSConfig{ - Url: url.MustGet(), + Url: u, Token: token, }) } diff --git a/pkg/monad/maybe.go b/pkg/monad/maybe.go index 247d9925..6c095259 100644 --- a/pkg/monad/maybe.go +++ b/pkg/monad/maybe.go @@ -54,6 +54,11 @@ func (m Maybe[T]) MustGet() T { return m.value } +// Get the inner value and a boolean indicating if it has been set. +func (m Maybe[T]) TryGet() (T, bool) { + return m.value, m.hasValue +} + // Retrieve the inner value or the fallback if it doesn't have one. func (m Maybe[T]) Get(fallback T) T { if !m.hasValue { diff --git a/pkg/monad/maybe_test.go b/pkg/monad/maybe_test.go index 1b55ed25..a4add8ff 100644 --- a/pkg/monad/maybe_test.go +++ b/pkg/monad/maybe_test.go @@ -28,6 +28,22 @@ func Test_Maybe(t *testing.T) { testutil.IsTrue(t, m.HasValue()) }) + t.Run("could returns its internal value and a boolean indicating if it has been set", func(t *testing.T) { + var m monad.Maybe[string] + + value, hasValue := m.TryGet() + + testutil.IsFalse(t, hasValue) + testutil.Equals(t, "", value) + + m = m.WithValue("ok") + + value, hasValue = m.TryGet() + + testutil.IsTrue(t, hasValue) + testutil.Equals(t, "ok", value) + }) + t.Run("could be assigned a value", func(t *testing.T) { var ( m monad.Maybe[time.Time] diff --git a/pkg/monad/patch.go b/pkg/monad/patch.go index 8fcad648..7307650d 100644 --- a/pkg/monad/patch.go +++ b/pkg/monad/patch.go @@ -22,6 +22,12 @@ func Nil[T any]() (p Patch[T]) { func (p Patch[T]) IsSet() bool { return p.isSet } func (p Patch[T]) IsNil() bool { return p.isSet && !p.hasValue } +// Try to get the inner optional value for this Patch structure. The boolean returns +// if the patch has been set so the returned value may represent a nil value. +func (p Patch[T]) TryGet() (Maybe[T], bool) { + return p.Maybe, p.isSet +} + // Implements the UnmarshalJSON interface. func (p *Patch[T]) UnmarshalJSON(data []byte) error { // If we're here, it means the value has been set diff --git a/pkg/monad/patch_test.go b/pkg/monad/patch_test.go index 3630234f..9773c8a2 100644 --- a/pkg/monad/patch_test.go +++ b/pkg/monad/patch_test.go @@ -34,6 +34,28 @@ func Test_Patch(t *testing.T) { testutil.IsFalse(t, p.HasValue()) }) + t.Run("should return the inner monad and a boolean indicating if it has been set", func(t *testing.T) { + tests := []struct { + name string + value monad.Patch[int] + isSet bool + hasValue bool + }{ + {"empty patch", monad.Patch[int]{}, false, false}, + {"nil patch", monad.Nil[int](), true, false}, + {"patch with a value", monad.PatchValue(42), true, true}, + } + + for _, test := range tests { + 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()) + }) + } + }) + t.Run("should correctly handle a JSON when unmarshalling", func(t *testing.T) { tests := []struct { json string diff --git a/pkg/storage/sqlite/builder/statements.go b/pkg/storage/sqlite/builder/statements.go index bd0b5dc5..03ba18e9 100644 --- a/pkg/storage/sqlite/builder/statements.go +++ b/pkg/storage/sqlite/builder/statements.go @@ -11,11 +11,13 @@ import ( // if the monad value is set. This is useful for optional fields in a query. func Maybe[T any](value monad.Maybe[T], fn func(T) (string, []any)) Statement { return func(builder sqlBuilder) { - if !value.HasValue() { + v, hasValue := value.TryGet() + + if !hasValue { return } - sql, args := fn(value.MustGet()) + sql, args := fn(v) builder.apply(sql, args...) } diff --git a/pkg/validation/check.go b/pkg/validation/check.go index 78d656ce..f51e4735 100644 --- a/pkg/validation/check.go +++ b/pkg/validation/check.go @@ -106,8 +106,8 @@ func If(expr bool, fn func() error) error { // Same as If but executes the fn if the monad has a value. func Maybe[T any](m monad.Maybe[T], fn func(T) error) error { - if m.HasValue() { - return fn(m.MustGet()) + if value, isSet := m.TryGet(); isSet { + return fn(value) } return nil @@ -119,8 +119,10 @@ func Maybe[T any](m monad.Maybe[T], fn func(T) error) error { // specific case. // FIXME: When go can infer correctly from an interface [T], merge Maybe and Patch. func Patch[T any](p monad.Patch[T], fn func(T) error) error { - if p.HasValue() { - return fn(p.MustGet()) + if maybeValue, isSet := p.TryGet(); isSet { + if value, hasValue := maybeValue.TryGet(); hasValue { + return fn(value) + } } return nil