Skip to content

Commit

Permalink
chore: wip, first steps towards #1
Browse files Browse the repository at this point in the history
Add the `Target` entity and refactor a lot of stuff.
Also closes #43 by updating the `update_app` PATCH behavior.
  • Loading branch information
YuukanOO committed Jan 25, 2024
1 parent 406947d commit 78e7969
Show file tree
Hide file tree
Showing 54 changed files with 1,067 additions and 648 deletions.
2 changes: 1 addition & 1 deletion DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Key aspects of seelf are:
- Reliable
- Easy to understand

_Althought Docker is the only backend supported at the moment, I would like to investigate to enable other ones too. Remote Docker or Podman for example._
_Althought Docker is the only provider supported at the moment, I would like to investigate to enable other ones too. Remote Docker or Podman for example._

## Installation

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ _The seelf initial public version has some limitation (only local Docker engine,

Got an already working docker compose file for your project ? Just send it to your [seelf](https://github.com/YuukanOO/seelf) instance and _boom_, that's live on your own infrastructure with all services correctly deployed and exposed on nice urls as needed! See [the documentation](DOCUMENTATION.md) for more information.

_Althought Docker is the only backend supported at the moment, I would like to investigate to enable other ones too. Remote Docker or Podman for example._
_Althought Docker is the only provider supported at the moment, I would like to investigate to enable other ones too. Remote Docker or Podman for example._

## Prerequisites

Expand Down
12 changes: 8 additions & 4 deletions api.http
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,17 @@ Content-Type: application/json
"vcs": {
"url": "https://git.voixdu.net/jleicher/go-api-example.git"
},
"env": {
"production": {
"production": {
"target": "de",
"vars": {
"app": {
"DEBUG": "false"
}
},
"staging": {
}
},
"staging": {
"target": "de",
"vars": {
"app": {
"DEBUG": "true"
}
Expand Down
2 changes: 1 addition & 1 deletion internal/auth/app/update_user/update_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func Handler(
}

if cmd.Email.HasValue() {
uniqueEmail, err := reader.IsEmailUniqueForUser(ctx, user.ID(), email)
uniqueEmail, err := reader.IsEmailUnique(ctx, email, user.ID())

if err != nil {
return "", err
Expand Down
15 changes: 15 additions & 0 deletions internal/auth/app/update_user/update_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ func Test_UpdateUser(t *testing.T) {
testutil.ErrorIs(t, apperr.ErrNotFound, err)
})

t.Run("should fail if the email is taken by another user", func(t *testing.T) {
passwordHash, _ := hasher.Hash("apassword")
user := domain.NewUser("john@doe.com", passwordHash, "anapikey")
anotherUser := domain.NewUser("jane@doe.com", passwordHash, "anapikey")

uc := sut(&user, &anotherUser)

_, err := uc(context.Background(), update_user.Command{
ID: string(user.ID()),
Email: monad.Value("jane@doe.com"),
})

testutil.ErrorIs(t, domain.ErrEmailAlreadyTaken, err)
})

t.Run("should succeed if values are the same", func(t *testing.T) {
passwordHash, _ := hasher.Hash("apassword")
user := domain.NewUser("john@doe.com", passwordHash, "anapikey")
Expand Down
3 changes: 1 addition & 2 deletions internal/auth/domain/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ type (
UsersReader interface {
GetUsersCount(context.Context) (uint, error)
GetIDFromAPIKey(context.Context, APIKey) (UserID, error)
IsEmailUnique(context.Context, Email) (UniqueEmail, error)
IsEmailUniqueForUser(context.Context, UserID, Email) (UniqueEmail, error)
IsEmailUnique(context.Context, Email, ...UserID) (UniqueEmail, error)
GetByEmail(context.Context, Email) (User, error)
GetByID(context.Context, UserID) (User, error)
}
Expand Down
21 changes: 4 additions & 17 deletions internal/auth/infra/memory/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package memory
import (
"context"
"errors"
"slices"

"github.com/YuukanOO/seelf/internal/auth/domain"
"github.com/YuukanOO/seelf/pkg/apperr"
Expand Down Expand Up @@ -39,10 +40,10 @@ func (s *usersStore) GetUsersCount(ctx context.Context) (uint, error) {
return uint(len(s.users)), nil
}

func (s *usersStore) IsEmailUnique(ctx context.Context, email domain.Email) (domain.UniqueEmail, error) {
_, err := s.GetByEmail(ctx, email)
func (s *usersStore) IsEmailUnique(ctx context.Context, email domain.Email, excluded ...domain.UserID) (domain.UniqueEmail, error) {
u, err := s.GetByEmail(ctx, email)

if errors.Is(err, apperr.ErrNotFound) {
if errors.Is(err, apperr.ErrNotFound) || slices.Contains(excluded, u.ID()) {
return domain.UniqueEmail(email), nil
}

Expand All @@ -53,20 +54,6 @@ func (s *usersStore) IsEmailUnique(ctx context.Context, email domain.Email) (dom
return "", err
}

func (s *usersStore) IsEmailUniqueForUser(ctx context.Context, id domain.UserID, email domain.Email) (domain.UniqueEmail, error) {
u, err := s.GetByEmail(ctx, email)

if errors.Is(err, apperr.ErrNotFound) || u.ID() == id {
return domain.UniqueEmail(email), nil
}

if err != nil {
return "", err
}

return "", domain.ErrEmailAlreadyTaken
}

func (s *usersStore) GetByID(ctx context.Context, id domain.UserID) (domain.User, error) {
for _, u := range s.users {
if u.id == id {
Expand Down
37 changes: 14 additions & 23 deletions internal/auth/infra/sqlite/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (

"github.com/YuukanOO/seelf/internal/auth/domain"
"github.com/YuukanOO/seelf/pkg/event"
"github.com/YuukanOO/seelf/pkg/monad"
"github.com/YuukanOO/seelf/pkg/storage/sqlite"
"github.com/YuukanOO/seelf/pkg/storage/sqlite/builder"
)
Expand All @@ -31,12 +30,21 @@ func (s *usersStore) GetUsersCount(ctx context.Context) (uint, error) {
Extract(s.db, ctx)
}

func (s *usersStore) IsEmailUnique(ctx context.Context, email domain.Email) (domain.UniqueEmail, error) {
return s.getUniqueEmail(ctx, email, monad.None[domain.UserID]())
}
func (s *usersStore) IsEmailUnique(ctx context.Context, email domain.Email, excluded ...domain.UserID) (domain.UniqueEmail, error) {
count, err := builder.
Query[uint]("SELECT COUNT(email) FROM users WHERE email = ?", email).
S(builder.Array("AND id NOT IN", excluded)).
Extract(s.db, ctx)

if err != nil {
return "", err
}

func (s *usersStore) IsEmailUniqueForUser(ctx context.Context, id domain.UserID, email domain.Email) (domain.UniqueEmail, error) {
return s.getUniqueEmail(ctx, email, monad.Value(id))
if count > 0 {
return "", domain.ErrEmailAlreadyTaken
}

return domain.UniqueEmail(email), nil
}

func (s *usersStore) GetByID(ctx context.Context, id domain.UserID) (u domain.User, err error) {
Expand Down Expand Up @@ -105,20 +113,3 @@ func (s *usersStore) Write(c context.Context, users ...*domain.User) error {
}
})
}

func (s *usersStore) getUniqueEmail(ctx context.Context, email domain.Email, uid monad.Maybe[domain.UserID]) (domain.UniqueEmail, error) {
count, err := builder.
Query[uint]("SELECT COUNT(email) FROM users WHERE email = ?", email).
S(builder.MaybeValue(uid, "AND id != ?")).
Extract(s.db, ctx)

if err != nil {
return "", err
}

if count > 0 {
return "", domain.ErrEmailAlreadyTaken
}

return domain.UniqueEmail(email), nil
}
6 changes: 3 additions & 3 deletions internal/deployment/app/cleanup_app/cleanup_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func Handler(
reader domain.AppsReader,
writer domain.AppsWriter,
artifactManager domain.ArtifactManager,
backend domain.Backend,
provider domain.Provider,
) bus.RequestHandler[bus.UnitType, Command] {
return func(ctx context.Context, cmd Command) (bus.UnitType, error) {
app, err := reader.GetByID(ctx, domain.AppID(cmd.ID))
Expand All @@ -50,12 +50,12 @@ func Handler(
return bus.Unit, err
}

// Before calling the backend cleanup, make sure the application can be safely deleted.
// Before calling the provider cleanup, make sure the application can be safely deleted.
if err = app.Delete(count); err != nil {
return bus.Unit, err
}

if err = backend.Cleanup(ctx, app); err != nil {
if err = provider.Cleanup(ctx, app); err != nil {
return bus.Unit, err
}

Expand Down
14 changes: 7 additions & 7 deletions internal/deployment/app/cleanup_app/cleanup_app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func Test_CleanupApp(t *testing.T) {
os.RemoveAll(opts.DataDir())
})

return cleanup_app.Handler(deploymentsStore, appsStore, appsStore, artifactManager, &dummyBackend{})
return cleanup_app.Handler(deploymentsStore, appsStore, appsStore, artifactManager, &dummyProvider{})
}

t.Run("should returns no error if the application does not exist", func(t *testing.T) {
Expand All @@ -50,7 +50,7 @@ func Test_CleanupApp(t *testing.T) {
})

t.Run("should fail if the application cleanup as not been requested", func(t *testing.T) {
a := domain.NewApp("my-app", "uid")
a := domain.NewApp("my-app", domain.NewEnvironmentConfig("1"), domain.NewEnvironmentConfig("1"), "uid")
uc := sut(initialData{
existingApps: []*domain.App{&a},
})
Expand All @@ -64,7 +64,7 @@ func Test_CleanupApp(t *testing.T) {
})

t.Run("should fail if there are still pending or running deployments", func(t *testing.T) {
a := domain.NewApp("my-app", "uid")
a := domain.NewApp("my-app", domain.NewEnvironmentConfig("1"), domain.NewEnvironmentConfig("1"), "uid")
depl, _ := a.NewDeployment(1, raw.Data(""), domain.Production, "uid")
a.RequestCleanup("uid")

Expand All @@ -82,7 +82,7 @@ func Test_CleanupApp(t *testing.T) {
})

t.Run("should succeed if everything is good", func(t *testing.T) {
a := domain.NewApp("my-app", "uid")
a := domain.NewApp("my-app", domain.NewEnvironmentConfig("1"), domain.NewEnvironmentConfig("1"), "uid")
a.RequestCleanup("uid")

uc := sut(initialData{
Expand All @@ -99,10 +99,10 @@ func Test_CleanupApp(t *testing.T) {
})
}

type dummyBackend struct {
domain.Backend
type dummyProvider struct {
domain.Provider
}

func (d *dummyBackend) Cleanup(ctx context.Context, app domain.App) error {
func (d *dummyProvider) Cleanup(ctx context.Context, app domain.App) error {
return nil
}
43 changes: 32 additions & 11 deletions internal/deployment/app/create_app/create_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ type (
Command struct {
bus.Command[string]

Name string `json:"name"`
VCS monad.Maybe[VCSConfig] `json:"vcs"`
Env monad.Maybe[map[string]map[string]map[string]string] `json:"env"` // This is not so sweet but hey!
Name string `json:"name"`
VCS monad.Maybe[VCSConfig] `json:"vcs"`
Production EnvironmentConfig `json:"production"`
Staging EnvironmentConfig `json:"staging"`
}

EnvironmentConfig struct {
Target string `json:"target"`
Vars monad.Maybe[map[string]map[string]string] `json:"vars"`
}

VCSConfig struct {
Expand All @@ -36,7 +42,6 @@ func Handler(
return func(ctx context.Context, cmd Command) (string, error) {
var (
appname domain.AppName
envs domain.EnvironmentsEnv
url domain.Url
)

Expand All @@ -50,20 +55,29 @@ func Handler(
}),
})
}),
"env": validation.Maybe(cmd.Env, func(envmap map[string]map[string]map[string]string) error {
return validation.Value(envmap, &envs, domain.EnvironmentsEnvFrom)
"production": validation.Check(validation.Of{
"target": validation.Is(cmd.Production.Target, strings.Required), // TODO: check target existence
}),
"staging": validation.Check(validation.Of{
"target": validation.Is(cmd.Staging.Target, strings.Required), // TODO: check target existence
}),
}); err != nil {
return "", err
}

// TODO: maybe the validation can be done when checking above
uniqueName, err := reader.IsNameUnique(ctx, appname)

if err != nil {
return "", validation.WrapIfAppErr(err, "name")
}

app := domain.NewApp(uniqueName, auth.CurrentUser(ctx).MustGet())
app := domain.NewApp(
uniqueName,
BuildEnvironmentConfig(domain.TargetID(cmd.Production.Target), cmd.Production.Vars),
BuildEnvironmentConfig(domain.TargetID(cmd.Staging.Target), cmd.Staging.Vars),
auth.CurrentUser(ctx).MustGet(),
)

if cmdVCS, isSet := cmd.VCS.TryGet(); isSet {
vcs := domain.NewVCSConfig(url)
Expand All @@ -75,14 +89,21 @@ func Handler(
app.UseVersionControl(vcs)
}

if cmd.Env.HasValue() {
app.HasEnvironmentVariables(envs)
}

if err := writer.Write(ctx, &app); err != nil {
return "", err
}

return string(app.ID()), nil
}
}

// Helper method to build a domain.EnvironmentConfig from a raw command value.
func BuildEnvironmentConfig(target domain.TargetID, env monad.Maybe[map[string]map[string]string]) domain.EnvironmentConfig {
config := domain.NewEnvironmentConfig(target)

if vars, hasVars := env.TryGet(); hasVars {
config = config.WithEnvironmentVariables(domain.ServicesEnvFrom(vars))
}

return config
}
14 changes: 13 additions & 1 deletion internal/deployment/app/create_app/create_app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ func Test_CreateApp(t *testing.T) {
})

t.Run("should fail if the name is already taken", func(t *testing.T) {
a := domain.NewApp("my-app", "uid")
a := domain.NewApp("my-app", domain.NewEnvironmentConfig("1"), domain.NewEnvironmentConfig("1"), "uid")
uc := sut(&a)

id, err := uc(ctx, create_app.Command{
Name: "my-app",
Production: create_app.EnvironmentConfig{
Target: "production-target",
},
Staging: create_app.EnvironmentConfig{
Target: "staging-target",
},
})

validationErr, ok := apperr.As[validation.Error](err)
Expand All @@ -47,6 +53,12 @@ func Test_CreateApp(t *testing.T) {
uc := sut()
id, err := uc(ctx, create_app.Command{
Name: "my-app",
Production: create_app.EnvironmentConfig{
Target: "production-target",
},
Staging: create_app.EnvironmentConfig{
Target: "staging-target",
},
})

testutil.IsNil(t, err)
Expand Down
Loading

0 comments on commit 78e7969

Please sign in to comment.