From e00ab397e2c77a929804f64bc8474a7d9cd80d12 Mon Sep 17 00:00:00 2001 From: adlerhurst <27845747+adlerhurst@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:45:54 +0100 Subject: [PATCH] show tim --- backend/domain/instance.go | 20 +- backend/domain/user.go | 2 +- backend/repository/cached/instance.go | 34 --- backend/repository/cached/user.go | 28 -- backend/repository/event/instance.go | 17 -- backend/repository/event/store.go | 16 -- backend/repository/event/user.go | 17 -- backend/repository/instance.go | 116 +++++++-- .../repository/orchestrate/handler/handle.go | 110 -------- backend/repository/orchestrate/instance.go | 100 ------- .../repository/orchestrate/instance_test.go | 244 ------------------ backend/repository/orchestrate/option.go | 35 --- backend/repository/orchestrate/user.go | 70 ----- backend/repository/sql/client.go | 21 -- backend/repository/sql/instance.go | 63 ----- backend/repository/sql/user.go | 29 --- backend/repository/telemetry/logged/global.go | 44 ---- backend/repository/telemetry/traced/global.go | 50 ---- backend/repository/user.go | 102 ++++++-- backend/storage/database/database.go | 8 + backend/telemetry/logging/logger.go | 42 ++- backend/telemetry/tracing/tracer.go | 61 +++-- .../img/zitadel_cluster_architecture.png | Bin 10389 -> 0 bytes .../img/zitadel_multicluster_architecture.png | Bin 7367 -> 0 bytes 24 files changed, 287 insertions(+), 942 deletions(-) delete mode 100644 backend/repository/cached/instance.go delete mode 100644 backend/repository/cached/user.go delete mode 100644 backend/repository/event/instance.go delete mode 100644 backend/repository/event/store.go delete mode 100644 backend/repository/event/user.go delete mode 100644 backend/repository/orchestrate/handler/handle.go delete mode 100644 backend/repository/orchestrate/instance.go delete mode 100644 backend/repository/orchestrate/instance_test.go delete mode 100644 backend/repository/orchestrate/option.go delete mode 100644 backend/repository/orchestrate/user.go delete mode 100644 backend/repository/sql/client.go delete mode 100644 backend/repository/sql/instance.go delete mode 100644 backend/repository/sql/user.go delete mode 100644 backend/repository/telemetry/logged/global.go delete mode 100644 backend/repository/telemetry/traced/global.go delete mode 100644 docs/static/img/zitadel_cluster_architecture.png delete mode 100644 docs/static/img/zitadel_multicluster_architecture.png diff --git a/backend/domain/instance.go b/backend/domain/instance.go index 44ab2f089e..8036274e60 100644 --- a/backend/domain/instance.go +++ b/backend/domain/instance.go @@ -4,7 +4,6 @@ import ( "context" "github.com/zitadel/zitadel/backend/repository" - "github.com/zitadel/zitadel/backend/repository/orchestrate" "github.com/zitadel/zitadel/backend/storage/database" "github.com/zitadel/zitadel/backend/telemetry/logging" "github.com/zitadel/zitadel/backend/telemetry/tracing" @@ -13,11 +12,11 @@ import ( type Instance struct { db database.Pool - instance instanceOrchestrator - user userOrchestrator + instance instanceRepository + user userRepository } -type instanceOrchestrator interface { +type instanceRepository interface { ByID(ctx context.Context, querier database.Querier, id string) (*repository.Instance, error) ByDomain(ctx context.Context, querier database.Querier, domain string) (*repository.Instance, error) Create(ctx context.Context, tx database.Transaction, instance *repository.Instance) (*repository.Instance, error) @@ -25,9 +24,15 @@ type instanceOrchestrator interface { func NewInstance(db database.Pool, tracer *tracing.Tracer, logger *logging.Logger) *Instance { b := &Instance{ - db: db, - instance: orchestrate.Instance(), - user: orchestrate.User(), + db: db, + instance: repository.NewInstance( + repository.WithLogger[repository.InstanceOptions](logger), + repository.WithTracer[repository.InstanceOptions](tracer), + ), + user: repository.NewUser( + repository.WithLogger[repository.UserOptions](logger), + repository.WithTracer[repository.UserOptions](tracer), + ), } return b @@ -59,5 +64,6 @@ func (b *Instance) SetUp(ctx context.Context, request *SetUpInstance) (err error return err } _, err = b.user.Create(ctx, tx, request.User) + b.authorizations.authorizeusers return err } diff --git a/backend/domain/user.go b/backend/domain/user.go index 971e6d6583..8df253c132 100644 --- a/backend/domain/user.go +++ b/backend/domain/user.go @@ -7,7 +7,7 @@ import ( "github.com/zitadel/zitadel/backend/storage/database" ) -type userOrchestrator interface { +type userRepository interface { Create(ctx context.Context, tx database.Transaction, user *repository.User) (*repository.User, error) ByID(ctx context.Context, querier database.Querier, id string) (*repository.User, error) } diff --git a/backend/repository/cached/instance.go b/backend/repository/cached/instance.go deleted file mode 100644 index 4859e06b65..0000000000 --- a/backend/repository/cached/instance.go +++ /dev/null @@ -1,34 +0,0 @@ -package cached - -import ( - "context" - "log" - - "github.com/zitadel/zitadel/backend/repository" - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type Instance struct { - cache.Cache[repository.InstanceIndex, string, *repository.Instance] -} - -func NewInstance(c cache.Cache[repository.InstanceIndex, string, *repository.Instance]) *Instance { - return &Instance{c} -} - -func (i *Instance) ByID(ctx context.Context, id string) *repository.Instance { - log.Println("cached.instance.byID") - instance, _ := i.Cache.Get(ctx, repository.InstanceByID, id) - return instance -} - -func (i *Instance) ByDomain(ctx context.Context, domain string) *repository.Instance { - log.Println("cached.instance.byDomain") - instance, _ := i.Cache.Get(ctx, repository.InstanceByDomain, domain) - return instance -} - -func (i *Instance) Set(ctx context.Context, instance *repository.Instance) { - log.Println("cached.instance.set") - i.Cache.Set(ctx, instance) -} diff --git a/backend/repository/cached/user.go b/backend/repository/cached/user.go deleted file mode 100644 index 9bb5c11a18..0000000000 --- a/backend/repository/cached/user.go +++ /dev/null @@ -1,28 +0,0 @@ -package cached - -import ( - "context" - "log" - - "github.com/zitadel/zitadel/backend/repository" - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type User struct { - cache.Cache[repository.UserIndex, string, *repository.User] -} - -func NewUser(c cache.Cache[repository.UserIndex, string, *repository.User]) *User { - return &User{c} -} - -func (i *User) ByID(ctx context.Context, id string) *repository.User { - log.Println("cached.user.byid") - user, _ := i.Cache.Get(ctx, repository.UserByIDIndex, id) - return user -} - -func (i *User) Set(ctx context.Context, user *repository.User) { - log.Println("cached.user.set") - i.Cache.Set(ctx, user) -} diff --git a/backend/repository/event/instance.go b/backend/repository/event/instance.go deleted file mode 100644 index 19fcce6b44..0000000000 --- a/backend/repository/event/instance.go +++ /dev/null @@ -1,17 +0,0 @@ -package event - -import ( - "context" - "log" - - "github.com/zitadel/zitadel/backend/repository" -) - -func (s *store) CreateInstance(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) { - log.Println("event.instance.create") - err := s.es.Push(ctx, instance) - if err != nil { - return nil, err - } - return instance, nil -} diff --git a/backend/repository/event/store.go b/backend/repository/event/store.go deleted file mode 100644 index eff0636a35..0000000000 --- a/backend/repository/event/store.go +++ /dev/null @@ -1,16 +0,0 @@ -package event - -import ( - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/storage/eventstore" -) - -type store struct { - es *eventstore.Eventstore -} - -func Store(client database.Executor) *store { - return &store{ - es: eventstore.New(client), - } -} diff --git a/backend/repository/event/user.go b/backend/repository/event/user.go deleted file mode 100644 index 71087043c0..0000000000 --- a/backend/repository/event/user.go +++ /dev/null @@ -1,17 +0,0 @@ -package event - -import ( - "context" - "log" - - "github.com/zitadel/zitadel/backend/repository" -) - -func (s *store) CreateUser(ctx context.Context, user *repository.User) (*repository.User, error) { - log.Println("event.user.create") - err := s.es.Push(ctx, user) - if err != nil { - return nil, err - } - return user, nil -} diff --git a/backend/repository/instance.go b/backend/repository/instance.go index 16ba2bea96..7ee923d641 100644 --- a/backend/repository/instance.go +++ b/backend/repository/instance.go @@ -1,37 +1,115 @@ package repository -import "github.com/zitadel/zitadel/backend/storage/cache" +import ( + "context" + + "github.com/zitadel/zitadel/backend/handler" + "github.com/zitadel/zitadel/backend/storage/database" + "github.com/zitadel/zitadel/backend/telemetry/logging" + "github.com/zitadel/zitadel/backend/telemetry/tracing" +) type Instance struct { ID string Name string } -type InstanceIndex uint8 - -var InstanceIndices = []InstanceIndex{ - InstanceByID, - InstanceByDomain, +type InstanceOptions struct { + cache *InstanceCache } -const ( - InstanceByID InstanceIndex = iota - InstanceByDomain -) +type instance struct { + options[InstanceOptions] + *InstanceOptions +} -var _ cache.Entry[InstanceIndex, string] = (*Instance)(nil) +func NewInstance(opts ...Option[InstanceOptions]) *instance { + i := new(instance) + i.InstanceOptions = &i.options.custom -// Keys implements [cache.Entry]. -func (i *Instance) Keys(index InstanceIndex) (key []string) { - switch index { - case InstanceByID: - return []string{i.ID} - case InstanceByDomain: - return []string{i.Name} + for _, opt := range opts { + opt.apply(&i.options) } - return nil + return i +} + +func WithInstanceCache(c *InstanceCache) Option[InstanceOptions] { + return func(opts *options[InstanceOptions]) { + opts.custom.cache = c + } +} + +func (i *instance) Create(ctx context.Context, tx database.Transaction, instance *Instance) (*Instance, error) { + return tracing.Wrap(i.tracer, "instance.SetUp", + handler.Chains( + handler.Decorates( + execute(tx).CreateInstance, + tracing.Decorate[*Instance, *Instance](i.tracer, tracing.WithSpanName("instance.sql.SetUp")), + logging.Decorate[*Instance, *Instance](i.logger, "instance.sql.SetUp"), + ), + handler.Decorates( + events(tx).CreateInstance, + tracing.Decorate[*Instance, *Instance](i.tracer, tracing.WithSpanName("instance.event.SetUp")), + logging.Decorate[*Instance, *Instance](i.logger, "instance.event.SetUp"), + ), + handler.SkipReturnPreviousHandler(i.cache, + handler.Decorates( + handler.NoReturnToHandle(i.cache.Set), + tracing.Decorate[*Instance, *Instance](i.tracer, tracing.WithSpanName("instance.cache.SetUp")), + logging.Decorate[*Instance, *Instance](i.logger, "instance.cache.SetUp"), + ), + ), + ), + )(ctx, instance) +} + +func (i *instance) ByID(ctx context.Context, querier database.Querier, id string) (*Instance, error) { + return tracing.Wrap(i.tracer, "instance.byID", + handler.SkipNext( + handler.SkipNilHandler(i.cache, + handler.ResFuncToHandle(i.cache.ByID), + ), + handler.Chain( + handler.Decorates( + query(querier).InstanceByID, + tracing.Decorate[string, *Instance](i.tracer, tracing.WithSpanName("instance.sql.ByID")), + logging.Decorate[string, *Instance](i.logger, "instance.sql.ByID"), + ), + handler.SkipNilHandler(i.cache, handler.NoReturnToHandle(i.cache.Set)), + ), + ), + )(ctx, id) +} + +func (i *instance) ByDomain(ctx context.Context, querier database.Querier, domain string) (*Instance, error) { + return tracing.Wrap(i.tracer, "instance.byDomain", + handler.SkipNext( + handler.SkipNilHandler(i.cache, + handler.ResFuncToHandle(i.cache.ByDomain), + ), + handler.Chain( + handler.Decorate( + query(querier).InstanceByDomain, + tracing.Decorate[string, *Instance](i.tracer, tracing.WithSpanName("instance.sql.ByDomain")), + ), + handler.SkipNilHandler(i.cache, handler.NoReturnToHandle(i.cache.Set)), + ), + ), + )(ctx, domain) } type ListRequest struct { Limit uint16 } + +func (i *instance) List(ctx context.Context, querier database.Querier, request *ListRequest) ([]*Instance, error) { + return tracing.Wrap(i.tracer, "instance.list", + handler.Chains( + handler.Decorates( + query(querier).ListInstances, + tracing.Decorate[*ListRequest, []*Instance](i.tracer, tracing.WithSpanName("instance.sql.List")), + logging.Decorate[*ListRequest, []*Instance](i.logger, "instance.sql.List"), + ), + ), + )(ctx, request) +} diff --git a/backend/repository/orchestrate/handler/handle.go b/backend/repository/orchestrate/handler/handle.go deleted file mode 100644 index c3dd46c17e..0000000000 --- a/backend/repository/orchestrate/handler/handle.go +++ /dev/null @@ -1,110 +0,0 @@ -package handler - -import ( - "context" -) - -// Handler is a function that handles the in. -type Handler[Out, In any] func(ctx context.Context, in Out) (out In, err error) - -// Decorator is a function that decorates the handle function. -type Decorator[In, Out any] func(ctx context.Context, in In, handle Handler[In, Out]) (out Out, err error) - -// Chain chains the handle function with the next handler. -// The next handler is called after the handle function. -func Chain[In, Out any](handle Handler[In, Out], next Handler[Out, Out]) Handler[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - out, err = handle(ctx, in) - if err != nil { - return out, err - } - return next(ctx, out) - } -} - -func Chains[In, Out any](handle Handler[In, Out], nexts ...Handler[Out, Out]) Handler[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - for _, next := range nexts { - handle = Chain(handle, next) - } - return handle(ctx, in) - } -} - -// Decorate decorates the handle function with the decorate function. -// The decorate function is called before the handle function. -func Decorate[In, Out any](handle Handler[In, Out], decorate Decorator[In, Out]) Handler[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - return decorate(ctx, in, handle) - } -} - -// Decorates decorates the handle function with the decorate functions. -// The decorates function is called before the handle function. -func Decorates[In, Out any](handle Handler[In, Out], decorates ...Decorator[In, Out]) Handler[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - for i := len(decorates) - 1; i >= 0; i-- { - handle = Decorate(handle, decorates[i]) - } - return handle(ctx, in) - } -} - -// SkipNext skips the next handler if the handle function returns a non-nil response. -func SkipNext[In, Out any](handle Handler[In, Out], next Handler[In, Out]) Handler[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - var empty Out - out, err = handle(ctx, in) - // TODO: does this work? - if any(out) != any(empty) || err != nil { - return out, err - } - return next(ctx, in) - } -} - -// SkipNilHandler skips the handle function if the handler is nil. -// If handle is nil, an empty output is returned. -// The function is safe to call with nil handler. -func SkipNilHandler[O, In, Out any](handler *O, handle Handler[In, Out]) Handler[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - if handler == nil { - return out, nil - } - return handle(ctx, in) - } -} - -// SkipReturnPreviousHandler skips the handle function if the handler is nil and returns the input. -// The function is safe to call with nil handler. -func SkipReturnPreviousHandler[O, In any](handler *O, handle Handler[In, In]) Handler[In, In] { - return func(ctx context.Context, in In) (out In, err error) { - if handler == nil { - return in, nil - } - return handle(ctx, in) - } -} - -func ResFuncToHandle[In any, Out any](fn func(context.Context, In) Out) Handler[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - return fn(ctx, in), nil - } -} - -func ErrFuncToHandle[In any](fn func(context.Context, In) error) Handler[In, In] { - return func(ctx context.Context, in In) (out In, err error) { - err = fn(ctx, in) - if err != nil { - return out, err - } - return in, nil - } -} - -func NoReturnToHandle[In any](fn func(context.Context, In)) Handler[In, In] { - return func(ctx context.Context, in In) (out In, err error) { - fn(ctx, in) - return in, nil - } -} diff --git a/backend/repository/orchestrate/instance.go b/backend/repository/orchestrate/instance.go deleted file mode 100644 index 3378c0163b..0000000000 --- a/backend/repository/orchestrate/instance.go +++ /dev/null @@ -1,100 +0,0 @@ -package orchestrate - -import ( - "context" - - "github.com/zitadel/zitadel/backend/repository" - "github.com/zitadel/zitadel/backend/repository/cached" - "github.com/zitadel/zitadel/backend/repository/event" - "github.com/zitadel/zitadel/backend/repository/orchestrate/handler" - "github.com/zitadel/zitadel/backend/repository/sql" - "github.com/zitadel/zitadel/backend/repository/telemetry/logged" - "github.com/zitadel/zitadel/backend/repository/telemetry/traced" - "github.com/zitadel/zitadel/backend/storage/cache" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -type InstanceOptions struct { - cache *cached.Instance -} - -type instance struct { - options[InstanceOptions] - *InstanceOptions -} - -func Instance(opts ...Option[InstanceOptions]) *instance { - i := new(instance) - i.InstanceOptions = &i.options.custom - - for _, opt := range opts { - opt.apply(&i.options) - } - return i -} - -func WithInstanceCache(c cache.Cache[repository.InstanceIndex, string, *repository.Instance]) Option[InstanceOptions] { - return func(opts *options[InstanceOptions]) { - opts.custom.cache = cached.NewInstance(c) - } -} - -func (i *instance) Create(ctx context.Context, tx database.Transaction, instance *repository.Instance) (*repository.Instance, error) { - return traced.Wrap(i.tracer, "instance.SetUp", - handler.Chains( - handler.Decorates( - sql.Execute(tx).CreateInstance, - traced.Decorate[*repository.Instance, *repository.Instance](i.tracer, tracing.WithSpanName("instance.sql.SetUp")), - logged.Decorate[*repository.Instance, *repository.Instance](i.logger, "instance.sql.SetUp"), - ), - handler.Decorates( - event.Store(tx).CreateInstance, - traced.Decorate[*repository.Instance, *repository.Instance](i.tracer, tracing.WithSpanName("instance.event.SetUp")), - logged.Decorate[*repository.Instance, *repository.Instance](i.logger, "instance.event.SetUp"), - ), - handler.SkipReturnPreviousHandler(i.cache, - handler.Decorates( - handler.NoReturnToHandle(i.cache.Set), - traced.Decorate[*repository.Instance, *repository.Instance](i.tracer, tracing.WithSpanName("instance.cache.SetUp")), - logged.Decorate[*repository.Instance, *repository.Instance](i.logger, "instance.cache.SetUp"), - ), - ), - ), - )(ctx, instance) -} - -func (i *instance) ByID(ctx context.Context, querier database.Querier, id string) (*repository.Instance, error) { - return traced.Wrap(i.tracer, "instance.byID", - handler.SkipNext( - handler.SkipNilHandler(i.cache, - handler.ResFuncToHandle(i.cache.ByID), - ), - handler.Chain( - handler.Decorates( - sql.Query(querier).InstanceByID, - traced.Decorate[string, *repository.Instance](i.tracer, tracing.WithSpanName("instance.sql.ByID")), - logged.Decorate[string, *repository.Instance](i.logger, "instance.sql.ByID"), - ), - handler.SkipNilHandler(i.cache, handler.NoReturnToHandle(i.cache.Set)), - ), - ), - )(ctx, id) -} - -func (i *instance) ByDomain(ctx context.Context, querier database.Querier, domain string) (*repository.Instance, error) { - return traced.Wrap(i.tracer, "instance.byDomain", - handler.SkipNext( - handler.SkipNilHandler(i.cache, - handler.ResFuncToHandle(i.cache.ByDomain), - ), - handler.Chain( - handler.Decorate( - sql.Query(querier).InstanceByDomain, - traced.Decorate[string, *repository.Instance](i.tracer, tracing.WithSpanName("instance.sql.ByDomain")), - ), - handler.SkipNilHandler(i.cache, handler.NoReturnToHandle(i.cache.Set)), - ), - ), - )(ctx, domain) -} diff --git a/backend/repository/orchestrate/instance_test.go b/backend/repository/orchestrate/instance_test.go deleted file mode 100644 index 1b2ba7f55f..0000000000 --- a/backend/repository/orchestrate/instance_test.go +++ /dev/null @@ -1,244 +0,0 @@ -package orchestrate_test - -import ( - "context" - "fmt" - "log/slog" - "os" - "reflect" - "testing" - - "github.com/zitadel/zitadel/backend/repository" - "github.com/zitadel/zitadel/backend/repository/orchestrate" - "github.com/zitadel/zitadel/backend/repository/sql" - "github.com/zitadel/zitadel/backend/storage/cache" - "github.com/zitadel/zitadel/backend/storage/cache/connector/gomap" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/storage/database/mock" - "github.com/zitadel/zitadel/backend/telemetry/logging" - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -func Test_instance_Create(t *testing.T) { - type args struct { - ctx context.Context - tx database.Transaction - instance *repository.Instance - } - tests := []struct { - name string - opts []orchestrate.Option[orchestrate.InstanceOptions] - args args - want *repository.Instance - wantErr bool - }{ - { - name: "simple", - opts: []orchestrate.Option[orchestrate.InstanceOptions]{ - orchestrate.WithTracer[orchestrate.InstanceOptions](tracing.NewTracer("test")), - orchestrate.WithLogger[orchestrate.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - orchestrate.WithInstanceCache( - gomap.NewCache[repository.InstanceIndex, string, *repository.Instance](context.Background(), repository.InstanceIndices, cache.Config{}), - ), - }, - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, mock.ExpectExec(sql.InstanceCreateStmt, "ID", "Name")), - instance: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - }, - want: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - wantErr: false, - }, - { - name: "without cache", - opts: []orchestrate.Option[orchestrate.InstanceOptions]{ - orchestrate.WithTracer[orchestrate.InstanceOptions](tracing.NewTracer("test")), - orchestrate.WithLogger[orchestrate.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - }, - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, mock.ExpectExec(sql.InstanceCreateStmt, "ID", "Name")), - instance: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - }, - want: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - wantErr: false, - }, - { - name: "without cache, tracer", - opts: []orchestrate.Option[orchestrate.InstanceOptions]{ - orchestrate.WithLogger[orchestrate.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - }, - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, mock.ExpectExec(sql.InstanceCreateStmt, "ID", "Name")), - instance: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - }, - want: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - wantErr: false, - }, - { - name: "without cache, tracer, logger", - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, mock.ExpectExec(sql.InstanceCreateStmt, "ID", "Name")), - instance: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - }, - want: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fmt.Printf("------------------------ %s ------------------------\n", tt.name) - i := orchestrate.Instance(tt.opts...) - got, err := i.Create(tt.args.ctx, tt.args.tx, tt.args.instance) - if (err != nil) != tt.wantErr { - t.Errorf("instance.Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("instance.Create() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_instance_ByID(t *testing.T) { - type args struct { - ctx context.Context - tx database.Transaction - id string - } - tests := []struct { - name string - opts []orchestrate.Option[orchestrate.InstanceOptions] - args args - want *repository.Instance - wantErr bool - }{ - { - name: "simple, not cached", - opts: []orchestrate.Option[orchestrate.InstanceOptions]{ - orchestrate.WithTracer[orchestrate.InstanceOptions](tracing.NewTracer("test")), - orchestrate.WithLogger[orchestrate.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - orchestrate.WithInstanceCache( - gomap.NewCache[repository.InstanceIndex, string, *repository.Instance](context.Background(), repository.InstanceIndices, cache.Config{}), - ), - }, - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, - mock.ExpectQueryRow(mock.NewRow(t, "id", "Name"), sql.InstanceByIDStmt, "id"), - ), - id: "id", - }, - want: &repository.Instance{ - ID: "id", - Name: "Name", - }, - wantErr: false, - }, - { - name: "simple, cached", - opts: []orchestrate.Option[orchestrate.InstanceOptions]{ - orchestrate.WithTracer[orchestrate.InstanceOptions](tracing.NewTracer("test")), - orchestrate.WithLogger[orchestrate.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - orchestrate.WithInstanceCache( - func() cache.Cache[repository.InstanceIndex, string, *repository.Instance] { - c := gomap.NewCache[repository.InstanceIndex, string, *repository.Instance](context.Background(), repository.InstanceIndices, cache.Config{}) - c.Set(context.Background(), &repository.Instance{ - ID: "id", - Name: "Name", - }) - return c - }(), - ), - }, - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, - mock.ExpectQueryRow(mock.NewRow(t, "id", "Name"), sql.InstanceByIDStmt, "id"), - ), - id: "id", - }, - want: &repository.Instance{ - ID: "id", - Name: "Name", - }, - wantErr: false, - }, - // { - // name: "without cache, tracer", - // opts: []orchestrate.Option[orchestrate.InstanceOptions]{ - // orchestrate.WithLogger[orchestrate.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - // }, - // args: args{ - // ctx: context.Background(), - // tx: mock.NewTransaction(), - // id: &repository.Instance{ - // ID: "ID", - // Name: "Name", - // }, - // }, - // want: &repository.Instance{ - // ID: "ID", - // Name: "Name", - // }, - // wantErr: false, - // }, - // { - // name: "without cache, tracer, logger", - // args: args{ - // ctx: context.Background(), - // tx: mock.NewTransaction(), - // id: &repository.Instance{ - // ID: "ID", - // Name: "Name", - // }, - // }, - // want: &repository.Instance{ - // ID: "ID", - // Name: "Name", - // }, - // wantErr: false, - // }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fmt.Printf("------------------------ %s ------------------------\n", tt.name) - i := orchestrate.Instance(tt.opts...) - got, err := i.ByID(tt.args.ctx, tt.args.tx, tt.args.id) - if (err != nil) != tt.wantErr { - t.Errorf("instance.Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("instance.Create() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/backend/repository/orchestrate/option.go b/backend/repository/orchestrate/option.go deleted file mode 100644 index efad690a47..0000000000 --- a/backend/repository/orchestrate/option.go +++ /dev/null @@ -1,35 +0,0 @@ -package orchestrate - -import ( - "github.com/zitadel/zitadel/backend/telemetry/logging" - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -// options are the default options for orchestrators. -type options[T any] struct { - custom T - defaultOptions -} - -type defaultOptions struct { - tracer *tracing.Tracer - logger *logging.Logger -} - -type Option[T any] func(*options[T]) - -func WithTracer[T any](tracer *tracing.Tracer) Option[T] { - return func(o *options[T]) { - o.tracer = tracer - } -} - -func WithLogger[T any](logger *logging.Logger) Option[T] { - return func(o *options[T]) { - o.logger = logger - } -} - -func (o Option[T]) apply(opts *options[T]) { - o(opts) -} diff --git a/backend/repository/orchestrate/user.go b/backend/repository/orchestrate/user.go deleted file mode 100644 index ff3de5bb10..0000000000 --- a/backend/repository/orchestrate/user.go +++ /dev/null @@ -1,70 +0,0 @@ -package orchestrate - -import ( - "context" - - "github.com/zitadel/zitadel/backend/repository" - "github.com/zitadel/zitadel/backend/repository/cached" - "github.com/zitadel/zitadel/backend/repository/event" - "github.com/zitadel/zitadel/backend/repository/orchestrate/handler" - "github.com/zitadel/zitadel/backend/repository/sql" - "github.com/zitadel/zitadel/backend/repository/telemetry/traced" - "github.com/zitadel/zitadel/backend/storage/cache" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -type UserOptions struct { - cache *cached.User -} - -type user struct { - options[UserOptions] - *UserOptions -} - -func User(opts ...Option[UserOptions]) *user { - i := new(user) - i.UserOptions = &i.options.custom - - for _, opt := range opts { - opt(&i.options) - } - return i -} - -func WithUserCache(cache cache.Cache[repository.UserIndex, string, *repository.User]) Option[UserOptions] { - return func(i *options[UserOptions]) { - i.custom.cache = cached.NewUser(cache) - } -} - -func (i *user) Create(ctx context.Context, tx database.Transaction, user *repository.User) (*repository.User, error) { - return traced.Wrap(i.tracer, "user.Create", - handler.Chain( - handler.Decorate( - sql.Execute(tx).CreateUser, - traced.Decorate[*repository.User, *repository.User](i.tracer, tracing.WithSpanName("user.sql.Create")), - ), - handler.Decorate( - event.Store(tx).CreateUser, - traced.Decorate[*repository.User, *repository.User](i.tracer, tracing.WithSpanName("user.event.Create")), - ), - ), - )(ctx, user) -} - -func (i *user) ByID(ctx context.Context, querier database.Querier, id string) (*repository.User, error) { - return handler.SkipNext( - handler.SkipNilHandler(i.cache, - handler.ResFuncToHandle(i.cache.ByID), - ), - handler.Chain( - handler.Decorate( - sql.Query(querier).UserByID, - traced.Decorate[string, *repository.User](i.tracer, tracing.WithSpanName("user.sql.ByID")), - ), - handler.SkipNilHandler(i.custom.cache, handler.NoReturnToHandle(i.cache.Set)), - ), - )(ctx, id) -} diff --git a/backend/repository/sql/client.go b/backend/repository/sql/client.go deleted file mode 100644 index 860397c0a6..0000000000 --- a/backend/repository/sql/client.go +++ /dev/null @@ -1,21 +0,0 @@ -package sql - -import ( - "github.com/zitadel/zitadel/backend/storage/database" -) - -type executor[C database.Executor] struct { - client C -} - -func Execute[C database.Executor](client C) *executor[C] { - return &executor[C]{client: client} -} - -type querier[C database.Querier] struct { - client C -} - -func Query[C database.Querier](client C) *querier[C] { - return &querier[C]{client: client} -} diff --git a/backend/repository/sql/instance.go b/backend/repository/sql/instance.go deleted file mode 100644 index 459912be3e..0000000000 --- a/backend/repository/sql/instance.go +++ /dev/null @@ -1,63 +0,0 @@ -package sql - -import ( - "context" - "log" - - "github.com/zitadel/zitadel/backend/repository" -) - -const InstanceByIDStmt = `SELECT id, name FROM instances WHERE id = $1` - -func (q *querier[C]) InstanceByID(ctx context.Context, id string) (*repository.Instance, error) { - log.Println("sql.instance.byID") - row := q.client.QueryRow(ctx, InstanceByIDStmt, id) - var instance repository.Instance - if err := row.Scan(&instance.ID, &instance.Name); err != nil { - return nil, err - } - return &instance, nil -} - -const instanceByDomainQuery = `SELECT i.id, i.name FROM instances i JOIN instance_domains id ON i.id = id.instance_id WHERE id.domain = $1` - -func (q *querier[C]) InstanceByDomain(ctx context.Context, domain string) (*repository.Instance, error) { - log.Println("sql.instance.byDomain") - row := q.client.QueryRow(ctx, instanceByDomainQuery, domain) - var instance repository.Instance - if err := row.Scan(&instance.ID, &instance.Name); err != nil { - return nil, err - } - return &instance, nil -} - -func (q *querier[C]) ListInstances(ctx context.Context, request *repository.ListRequest) (res []*repository.Instance, err error) { - log.Println("sql.instance.list") - rows, err := q.client.Query(ctx, "SELECT id, name FROM instances") - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - var instance repository.Instance - if err = rows.Scan(&instance.ID, &instance.Name); err != nil { - return nil, err - } - res = append(res, &instance) - } - if err = rows.Err(); err != nil { - return nil, err - } - return res, nil -} - -const InstanceCreateStmt = `INSERT INTO instances (id, name) VALUES ($1, $2)` - -func (e *executor[C]) CreateInstance(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) { - log.Println("sql.instance.create") - err := e.client.Exec(ctx, InstanceCreateStmt, instance.ID, instance.Name) - if err != nil { - return nil, err - } - return instance, nil -} diff --git a/backend/repository/sql/user.go b/backend/repository/sql/user.go deleted file mode 100644 index d43bae1772..0000000000 --- a/backend/repository/sql/user.go +++ /dev/null @@ -1,29 +0,0 @@ -package sql - -import ( - "context" - "log" - - "github.com/zitadel/zitadel/backend/repository" -) - -const userByIDQuery = `SELECT id, username FROM users WHERE id = $1` - -func (q *querier[C]) UserByID(ctx context.Context, id string) (res *repository.User, err error) { - log.Println("sql.user.byID") - row := q.client.QueryRow(ctx, userByIDQuery, id) - var user repository.User - if err := row.Scan(&user.ID, &user.Username); err != nil { - return nil, err - } - return &user, nil -} - -func (e *executor[C]) CreateUser(ctx context.Context, user *repository.User) (res *repository.User, err error) { - log.Println("sql.user.create") - err = e.client.Exec(ctx, "INSERT INTO users (id, username) VALUES ($1, $2)", user.ID, user.Username) - if err != nil { - return nil, err - } - return user, nil -} diff --git a/backend/repository/telemetry/logged/global.go b/backend/repository/telemetry/logged/global.go deleted file mode 100644 index 733cdee118..0000000000 --- a/backend/repository/telemetry/logged/global.go +++ /dev/null @@ -1,44 +0,0 @@ -package logged - -import ( - "context" - "log" - "log/slog" - - "github.com/zitadel/zitadel/backend/repository/orchestrate/handler" - "github.com/zitadel/zitadel/backend/telemetry/logging" -) - -// Wrap decorates the given handle function with logging. -// The function is safe to call with nil logger. -func Wrap[Req, Res any](logger *logging.Logger, name string, handle handler.Handler[Req, Res]) handler.Handler[Req, Res] { - if logger == nil { - return handle - } - return func(ctx context.Context, r Req) (_ Res, err error) { - logger.Debug("execute", slog.String("handler", name)) - defer logger.Debug("done", slog.String("handler", name)) - log.Println("log.wrap", name) - return handle(ctx, r) - } -} - -// Decorate decorates the given handle function with logging. -// The function is safe to call with nil logger. -func Decorate[Req, Res any](logger *logging.Logger, name string) handler.Decorator[Req, Res] { - return func(ctx context.Context, request Req, handle handler.Handler[Req, Res]) (res Res, err error) { - if logger == nil { - return handle(ctx, request) - } - logger = logger.With("handler", name) - logger.DebugContext(ctx, "execute") - log.Println("logged.decorate", name) - defer func() { - if err != nil { - logger.ErrorContext(ctx, "failed", slog.String("cause", err.Error())) - } - logger.DebugContext(ctx, "done") - }() - return handle(ctx, request) - } -} diff --git a/backend/repository/telemetry/traced/global.go b/backend/repository/telemetry/traced/global.go deleted file mode 100644 index 91ecdb88b8..0000000000 --- a/backend/repository/telemetry/traced/global.go +++ /dev/null @@ -1,50 +0,0 @@ -package traced - -import ( - "context" - "log" - - "github.com/zitadel/zitadel/backend/repository/orchestrate/handler" - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -// Wrap decorates the given handle function with tracing. -// The function is safe to call with nil tracer. -func Wrap[Req, Res any](tracer *tracing.Tracer, name string, handle handler.Handler[Req, Res]) handler.Handler[Req, Res] { - if tracer == nil { - return handle - } - return func(ctx context.Context, r Req) (_ Res, err error) { - ctx, span := tracer.Start( - ctx, - name, - ) - log.Println("trace.wrap", name) - defer func() { - if err != nil { - span.RecordError(err) - } - span.End() - }() - return handle(ctx, r) - } -} - -// Decorate decorates the given handle function with tracing. -// The function is safe to call with nil tracer. -func Decorate[Req, Res any](tracer *tracing.Tracer, opts ...tracing.DecorateOption) handler.Decorator[Req, Res] { - return func(ctx context.Context, r Req, handle handler.Handler[Req, Res]) (_ Res, err error) { - if tracer == nil { - return handle(ctx, r) - } - o := new(tracing.DecorateOptions) - for _, opt := range opts { - opt(o) - } - log.Println("traced.decorate") - - ctx, end := o.Start(ctx, tracer) - defer end(err) - return handle(ctx, r) - } -} diff --git a/backend/repository/user.go b/backend/repository/user.go index 1a5dc80279..a44cfbd30e 100644 --- a/backend/repository/user.go +++ b/backend/repository/user.go @@ -1,33 +1,97 @@ package repository -import "github.com/zitadel/zitadel/backend/storage/cache" +import ( + "context" + + "github.com/zitadel/zitadel/backend/handler" + "github.com/zitadel/zitadel/backend/storage/database" + "github.com/zitadel/zitadel/backend/telemetry/tracing" + "github.com/zitadel/zitadel/internal/crypto" +) type User struct { ID string Username string } -type UserIndex uint8 - -var UserIndices = []UserIndex{ - UserByIDIndex, - UserByUsernameIndex, +type UserOptions struct { + cache *UserCache } -const ( - UserByIDIndex UserIndex = iota - UserByUsernameIndex -) +type user struct { + options[UserOptions] + *UserOptions +} -var _ cache.Entry[UserIndex, string] = (*User)(nil) +func NewUser(opts ...Option[UserOptions]) *user { + i := new(user) + i.UserOptions = &i.options.custom -// Keys implements [cache.Entry]. -func (u *User) Keys(index UserIndex) (key []string) { - switch index { - case UserByIDIndex: - return []string{u.ID} - case UserByUsernameIndex: - return []string{u.Username} + for _, opt := range opts { + opt(&i.options) } - return nil + return i } + +func WithUserCache(c *UserCache) Option[UserOptions] { + return func(i *options[UserOptions]) { + i.custom.cache = c + } +} + +func (u *user) Create(ctx context.Context, tx database.Transaction, user *User) (*User, error) { + return tracing.Wrap(u.tracer, "user.Create", + handler.Chain( + handler.Decorate( + execute(tx).CreateUser, + tracing.Decorate[*User, *User](u.tracer, tracing.WithSpanName("user.sql.Create")), + ), + handler.Decorate( + events(tx).CreateUser, + tracing.Decorate[*User, *User](u.tracer, tracing.WithSpanName("user.event.Create")), + ), + ), + )(ctx, user) +} + +func (u *user) ByID(ctx context.Context, client database.Querier, id string) (*User, error) { + return handler.SkipNext( + handler.SkipNilHandler(u.cache, + handler.ResFuncToHandle(u.cache.ByID), + ), + handler.Chain( + handler.Decorate( + query(client).UserByID, + tracing.Decorate[string, *User](u.tracer, tracing.WithSpanName("user.sql.ByID")), + ), + handler.SkipNilHandler(u.custom.cache, handler.NoReturnToHandle(u.cache.Set)), + ), + )(ctx, id) +} + +type ChangeEmail struct { + UserID string + Email string + Opt *ChangeEmailOption +} + +type ChangeEmailOption struct { + returnCode bool + isVerified bool + sendCode bool +} + +type ChangeEmailVerifiedOption struct { + isVerified bool +} + +type ChangeEmailReturnCodeOption struct { + alg crypto.EncryptionAlgorithm +} + +type ChangeEmailSendCodeOption struct { + alg crypto.EncryptionAlgorithm + urlTemplate string +} + +func (u *user) ChangeEmail(ctx context.Context, client database.Executor, change *ChangeEmail) diff --git a/backend/storage/database/database.go b/backend/storage/database/database.go index 2108f36423..7a4dc932ae 100644 --- a/backend/storage/database/database.go +++ b/backend/storage/database/database.go @@ -72,6 +72,14 @@ type Querier interface { QueryRow(ctx context.Context, stmt string, args ...any) Row } +func Query[Out any](q Querier, fn func(q Querier) ([]Out, error)) ([]Out, error) { + return fn(q) +} + +func QueryRow[Out any](q Querier, fn func(q Querier) (Out, error)) (Out, error) { + return fn(q) +} + type Executor interface { Exec(ctx context.Context, stmt string, args ...any) error } diff --git a/backend/telemetry/logging/logger.go b/backend/telemetry/logging/logger.go index c452d1021f..5a29d8aa01 100644 --- a/backend/telemetry/logging/logger.go +++ b/backend/telemetry/logging/logger.go @@ -1,6 +1,12 @@ package logging -import "log/slog" +import ( + "context" + "log" + "log/slog" + + "github.com/zitadel/zitadel/backend/handler" +) type Logger struct { *slog.Logger @@ -13,3 +19,37 @@ func New(l *slog.Logger) *Logger { func (l *Logger) With(args ...any) *Logger { return &Logger{l.Logger.With(args...)} } + +// Wrap decorates the given handle function with +// The function is safe to call with nil logger. +func Wrap[Req, Res any](logger *Logger, name string, handle handler.Handle[Req, Res]) handler.Handle[Req, Res] { + if logger == nil { + return handle + } + return func(ctx context.Context, r Req) (_ Res, err error) { + logger.Debug("execute", slog.String("handler", name)) + defer logger.Debug("done", slog.String("handler", name)) + log.Println("log.wrap", name) + return handle(ctx, r) + } +} + +// Decorate decorates the given handle function with logging. +// The function is safe to call with nil logger. +func Decorate[Req, Res any](logger *Logger, name string) handler.Middleware[Req, Res] { + return func(ctx context.Context, request Req, handle handler.Handle[Req, Res]) (res Res, err error) { + if logger == nil { + return handle(ctx, request) + } + logger = logger.With("handler", name) + logger.DebugContext(ctx, "execute") + log.Println("logged.decorate", name) + defer func() { + if err != nil { + logger.ErrorContext(ctx, "failed", slog.String("cause", err.Error())) + } + logger.DebugContext(ctx, "done") + }() + return handle(ctx, request) + } +} diff --git a/backend/telemetry/tracing/tracer.go b/backend/telemetry/tracing/tracer.go index 6c0801fb2c..d3634075e1 100644 --- a/backend/telemetry/tracing/tracer.go +++ b/backend/telemetry/tracing/tracer.go @@ -2,10 +2,13 @@ package tracing import ( "context" + "log" "runtime" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" + + "github.com/zitadel/zitadel/backend/handler" ) type Tracer struct{ trace.Tracer } @@ -43,6 +46,47 @@ func WithSpanEndOptions(opts ...trace.SpanEndOption) DecorateOption { } } +// Wrap decorates the given handle function with tracing. +// The function is safe to call with nil tracer. +func Wrap[Req, Res any](tracer *Tracer, name string, handle handler.Handle[Req, Res]) handler.Handle[Req, Res] { + if tracer == nil { + return handle + } + return func(ctx context.Context, r Req) (_ Res, err error) { + ctx, span := tracer.Start( + ctx, + name, + ) + log.Println("trace.wrap", name) + defer func() { + if err != nil { + span.RecordError(err) + } + span.End() + }() + return handle(ctx, r) + } +} + +// Decorate decorates the given handle function with +// The function is safe to call with nil tracer. +func Decorate[Req, Res any](tracer *Tracer, opts ...DecorateOption) handler.Middleware[Req, Res] { + return func(ctx context.Context, r Req, handle handler.Handle[Req, Res]) (_ Res, err error) { + if tracer == nil { + return handle(ctx, r) + } + o := new(DecorateOptions) + for _, opt := range opts { + opt(o) + } + log.Println("traced.decorate") + + ctx, end := o.Start(ctx, tracer) + defer end(err) + return handle(ctx, r) + } +} + func (o *DecorateOptions) Start(ctx context.Context, tracer *Tracer) (context.Context, func(error)) { if o.spanName == "" { o.spanName = functionName() @@ -56,23 +100,6 @@ func (o *DecorateOptions) end(err error) { o.span.End(o.endOpts...) } -// func (t Tracer) Decorate(ctx context.Context, fn func(ctx context.Context) error, opts ...DecorateOption) { -// o := new(DecorateOptions) -// for _, opt := range opts { -// opt(o) -// } - -// if o.spanName == "" { -// o.spanName = functionName() -// } - -// ctx, span := t.Tracer.Start(ctx, o.spanName, o.startOpts...) -// defer span.End(o.endOpts...) - -// err := fn(ctx) -// span.RecordError(err) -// } - func functionName() string { counter, _, _, success := runtime.Caller(2) diff --git a/docs/static/img/zitadel_cluster_architecture.png b/docs/static/img/zitadel_cluster_architecture.png deleted file mode 100644 index a968610b70560550551b0879a9a3ba899c166888..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10389 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 E9}NdBU;qFB