This commit is contained in:
adlerhurst
2025-03-15 08:32:53 +01:00
parent 01499d77c7
commit bc6b1d3fcf
21 changed files with 456 additions and 556 deletions

View File

@@ -5,6 +5,7 @@ import (
"sync"
"github.com/zitadel/zitadel/backend/repository"
"github.com/zitadel/zitadel/backend/repository/orchestrate/handler"
"github.com/zitadel/zitadel/backend/storage/cache"
)
@@ -12,69 +13,88 @@ type Instance struct {
mu *sync.RWMutex
byID cache.Cache[string, *repository.Instance]
byDomain cache.Cache[string, *repository.Instance]
next repository.InstanceRepository
}
func (i *Instance) SetNext(next repository.InstanceRepository) *Instance {
return &Instance{
mu: i.mu,
byID: i.byID,
byDomain: i.byDomain,
next: next,
}
}
func SetUpInstance(
cache *Instance,
handle handler.Handle[*repository.Instance, *repository.Instance],
) handler.Handle[*repository.Instance, *repository.Instance] {
return func(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) {
instance, err := handle(ctx, instance)
if err != nil {
return nil, err
}
// ByDomain implements repository.InstanceRepository.
func (i *Instance) ByDomain(ctx context.Context, domain string) (instance *repository.Instance, err error) {
i.mu.RLock()
defer i.mu.RUnlock()
if instance, ok := i.byDomain.Get(domain); ok {
cache.set(instance, "")
return instance, nil
}
instance, err = i.next.ByDomain(ctx, domain)
if err != nil {
return nil, err
}
i.set(instance, domain)
return instance, nil
}
// ByID implements repository.InstanceRepository.
func (i *Instance) ByID(ctx context.Context, id string) (*repository.Instance, error) {
i.mu.RLock()
defer i.mu.RUnlock()
if instance, ok := i.byID.Get(id); ok {
func SetUpInstanceWithout(cache *Instance) handler.Handle[*repository.Instance, *repository.Instance] {
return func(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) {
cache.set(instance, "")
return instance, nil
}
instance, err := i.next.ByID(ctx, id)
if err != nil {
return nil, err
}
i.set(instance, "")
return instance, nil
}
// SetUp implements repository.InstanceRepository.
func (i *Instance) SetUp(ctx context.Context, instance *repository.Instance) error {
err := i.next.SetUp(ctx, instance)
if err != nil {
return err
}
func SetUpInstanceDecorated(
cache *Instance,
handle handler.Handle[*repository.Instance, *repository.Instance],
decorator handler.Decorate[*repository.Instance, *repository.Instance],
) handler.Handle[*repository.Instance, *repository.Instance] {
return func(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) {
instance, err := handle(ctx, instance)
if err != nil {
return nil, err
}
i.set(instance, "")
return nil
return decorator(ctx, instance, func(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) {
cache.set(instance, "")
return instance, nil
})
}
}
var _ repository.InstanceRepository = (*Instance)(nil)
func ForInstanceByID(cache *Instance, handle handler.Handle[string, *repository.Instance]) handler.Handle[string, *repository.Instance] {
return func(ctx context.Context, id string) (*repository.Instance, error) {
cache.mu.RLock()
instance, ok := cache.byID.Get(id)
cache.mu.RUnlock()
if ok {
return instance, nil
}
instance, err := handle(ctx, id)
if err != nil {
return nil, err
}
cache.set(instance, "")
return instance, nil
}
}
func ForInstanceByDomain(cache *Instance, handle handler.Handle[string, *repository.Instance]) handler.Handle[string, *repository.Instance] {
return func(ctx context.Context, domain string) (*repository.Instance, error) {
cache.mu.RLock()
instance, ok := cache.byDomain.Get(domain)
cache.mu.RUnlock()
if ok {
return instance, nil
}
instance, err := handle(ctx, domain)
if err != nil {
return nil, err
}
cache.set(instance, domain)
return instance, nil
}
}
func (i *Instance) set(instance *repository.Instance, domain string) {
i.mu.Lock()

View File

@@ -4,34 +4,59 @@ import (
"context"
"github.com/zitadel/zitadel/backend/repository"
"github.com/zitadel/zitadel/backend/repository/orchestrate/handler"
"github.com/zitadel/zitadel/backend/storage/database"
"github.com/zitadel/zitadel/backend/storage/eventstore"
)
var _ repository.InstanceRepository = (*Instance)(nil)
func SetUpInstance(
client database.Executor,
next handler.Handle[*repository.Instance, *repository.Instance],
) handler.Handle[*repository.Instance, *repository.Instance] {
es := eventstore.New(client)
return func(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) {
instance, err := next(ctx, instance)
if err != nil {
return nil, err
}
type Instance struct {
*eventstore.Eventstore
next repository.InstanceRepository
}
func NewInstance(eventstore *eventstore.Eventstore, next repository.InstanceRepository) *Instance {
return &Instance{next: next, Eventstore: eventstore}
}
func (i *Instance) ByID(ctx context.Context, id string) (*repository.Instance, error) {
return i.next.ByID(ctx, id)
}
func (i *Instance) ByDomain(ctx context.Context, domain string) (*repository.Instance, error) {
return i.next.ByDomain(ctx, domain)
}
func (i *Instance) SetUp(ctx context.Context, instance *repository.Instance) error {
err := i.next.SetUp(ctx, instance)
if err != nil {
return err
err = es.Push(ctx, instance)
if err != nil {
return nil, err
}
return instance, nil
}
}
func SetUpInstanceWithout(client database.Executor) handler.Handle[*repository.Instance, *repository.Instance] {
es := eventstore.New(client)
return func(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) {
err := es.Push(ctx, instance)
if err != nil {
return nil, err
}
return instance, nil
}
}
func SetUpInstanceDecorated(
client database.Executor,
next handler.Handle[*repository.Instance, *repository.Instance],
decorate handler.Decorate[*repository.Instance, *repository.Instance],
) handler.Handle[*repository.Instance, *repository.Instance] {
es := eventstore.New(client)
return func(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) {
instance, err := next(ctx, instance)
if err != nil {
return nil, err
}
return decorate(ctx, instance, func(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) {
err = es.Push(ctx, instance)
if err != nil {
return nil, err
}
return instance, nil
})
}
return i.Push(ctx, instance)
}

View File

@@ -1,5 +0,0 @@
package repository
type Handler interface {
SetNext(next Handler) Handler
}

View File

@@ -1,38 +1,10 @@
package repository
import "context"
type InstanceRepository interface {
InstanceSetuper
instanceByIDQuerier
instanceByDomainQuerier
}
type Instance struct {
ID string
Name string
}
type SetUpInstance func(ctx context.Context, instance *Instance) error
type InstanceSetuper interface {
SetUp(ctx context.Context, instance *Instance) error
}
type InstanceByID func(ctx context.Context, id string) (*Instance, error)
type instanceByIDQuerier interface {
ByID(ctx context.Context, id string) (*Instance, error)
}
type InstanceByDomain func(ctx context.Context, domain string) (*Instance, error)
type instanceByDomainQuerier interface {
ByDomain(ctx context.Context, domain string) (*Instance, error)
}
type ListInstances func(ctx context.Context) ([]*Instance, error)
type InstanceLister interface {
List(ctx context.Context) ([]*Instance, error)
type ListRequest struct {
Limit uint16
}

View File

@@ -0,0 +1,26 @@
package handler
import "context"
type Handle[Req, Res any] func(ctx context.Context, request Req) (res Res, err error)
type Decorate[Req, Res any] func(ctx context.Context, request Req, handle Handle[Req, Res]) (res Res, err error)
func NewChained[Req, Res any](handle Handle[Req, Res], next Handle[Res, Res]) Handle[Req, Res] {
return func(ctx context.Context, request Req) (res Res, err error) {
res, err = handle(ctx, request)
if err != nil {
return res, err
}
if next == nil {
return res, nil
}
return next(ctx, res)
}
}
func NewDecorated[Req, Res any](decorate Decorate[Req, Res], handle Handle[Req, Res]) Handle[Req, Res] {
return func(ctx context.Context, request Req) (res Res, err error) {
return decorate(ctx, request, handle)
}
}

View File

@@ -0,0 +1,72 @@
package orchestrate
import (
"context"
"github.com/zitadel/zitadel/backend/repository"
"github.com/zitadel/zitadel/backend/repository/cache"
"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/database"
"github.com/zitadel/zitadel/backend/telemetry/tracing"
)
type instance struct {
options
cache *cache.Instance
}
func Instance(opts ...Option) *instance {
i := new(instance)
for _, opt := range opts {
opt(&i.options)
}
return i
}
func (i *instance) apply(o Option) {
o(&i.options)
}
func (i *instance) SetUp(ctx context.Context, tx database.Transaction, instance *repository.Instance) (*repository.Instance, error) {
return handler.NewChained(
handler.NewDecorated(
traced.DecorateHandle[*repository.Instance, *repository.Instance](i.tracer, tracing.WithSpanName("instance.sql.SetUp")),
sql.SetUpInstance(tx),
),
handler.NewChained(
handler.NewDecorated(
traced.DecorateHandle[*repository.Instance, *repository.Instance](i.tracer, tracing.WithSpanName("instance.event.SetUp")),
event.SetUpInstanceWithout(tx),
),
handler.NewDecorated(
traced.DecorateHandle[*repository.Instance, *repository.Instance](i.tracer, tracing.WithSpanName("instance.cache.SetUp")),
cache.SetUpInstanceWithout(i.cache),
),
),
)(ctx, instance)
}
func (i *instance) ByID(ctx context.Context, querier database.Querier, id string) (*repository.Instance, error) {
return traced.Wrap(i.tracer, "instance.byID",
logged.Wrap(i.logger, "instance.byID",
cache.ForInstanceByID(i.cache,
sql.InstanceByID(querier),
),
),
)(ctx, id)
}
func (i *instance) ByDomain(ctx context.Context, querier database.Querier, domain string) (*repository.Instance, error) {
return traced.Wrap(i.tracer, "instance.byDomain",
logged.Wrap(i.logger, "instance.byDomain",
cache.ForInstanceByDomain(i.cache,
sql.InstanceByDomain(querier),
),
),
)(ctx, domain)
}

View File

@@ -0,0 +1,25 @@
package orchestrate
import (
"github.com/zitadel/zitadel/backend/telemetry/logging"
"github.com/zitadel/zitadel/backend/telemetry/tracing"
)
type options struct {
tracer *tracing.Tracer
logger *logging.Logger
}
type Option func(*options)
func WithTracer(tracer *tracing.Tracer) Option {
return func(o *options) {
o.tracer = tracer
}
}
func WithLogger(logger *logging.Logger) Option {
return func(o *options) {
o.logger = logger
}
}

View File

@@ -4,42 +4,42 @@ import (
"context"
"github.com/zitadel/zitadel/backend/repository"
"github.com/zitadel/zitadel/backend/repository/orchestrate/handler"
"github.com/zitadel/zitadel/backend/storage/database"
)
func NewInstance(client database.QueryExecutor) repository.InstanceRepository {
return &Instance{client: client}
}
type Instance struct {
client database.QueryExecutor
}
const instanceByDomainQuery = `SELECT i.id, i.name FROM instances i JOIN instance_domains id ON i.id = id.instance_id WHERE id.domain = $1`
// ByDomain implements [InstanceRepository].
func (r *Instance) ByDomain(ctx context.Context, domain string) (*repository.Instance, error) {
row := r.client.QueryRow(ctx, instanceByDomainQuery, domain)
var instance repository.Instance
if err := row.Scan(&instance.ID, &instance.Name); err != nil {
return nil, err
func InstanceByDomain(client database.Querier) handler.Handle[string, *repository.Instance] {
return func(ctx context.Context, domain string) (*repository.Instance, error) {
row := 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
}
return &instance, nil
}
const instanceByIDQuery = `SELECT id, name FROM instances WHERE id = $1`
// ByID implements [InstanceRepository].
func (r *Instance) ByID(ctx context.Context, id string) (*repository.Instance, error) {
row := r.client.QueryRow(ctx, instanceByIDQuery, id)
var instance repository.Instance
if err := row.Scan(&instance.ID, &instance.Name); err != nil {
return nil, err
func InstanceByID(client database.Querier) handler.Handle[string, *repository.Instance] {
return func(ctx context.Context, id string) (*repository.Instance, error) {
row := client.QueryRow(ctx, instanceByIDQuery, id)
var instance repository.Instance
if err := row.Scan(&instance.ID, &instance.Name); err != nil {
return nil, err
}
return &instance, nil
}
return &instance, nil
}
// SetUp implements [InstanceRepository].
func (r *Instance) SetUp(ctx context.Context, instance *repository.Instance) error {
return r.client.Exec(ctx, "INSERT INTO instances (id, name) VALUES ($1, $2)", instance.ID, instance.Name)
func SetUpInstance(tx database.Transaction) handler.Handle[*repository.Instance, *repository.Instance] {
return func(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) {
err := tx.Exec(ctx, "INSERT INTO instances (id, name) VALUES ($1, $2)", instance.ID, instance.Name)
if err != nil {
return nil, err
}
return instance, nil
}
}

View File

@@ -0,0 +1,68 @@
package logged
import (
"context"
"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.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))
return handle(ctx, r)
}
}
func WrapInside(logger *logging.Logger, name string) func(ctx context.Context, fn func(context.Context) error) {
logger = logger.With(slog.String("handler", name))
return func(ctx context.Context, fn func(context.Context) error) {
logger.Debug("execute")
var err error
defer func() {
if err != nil {
logger.Error("failed", slog.String("cause", err.Error()))
}
logger.Debug("done")
}()
err = fn(ctx)
}
}
func DecorateHandle[Req, Res any](logger *logging.Logger, handle func(context.Context, Req) (Res, error)) func(ctx context.Context, r Req) (_ Res, err error) {
return func(ctx context.Context, r Req) (_ Res, err error) {
logger.DebugContext(ctx, "execute")
defer func() {
if err != nil {
logger.ErrorContext(ctx, "failed", slog.String("cause", err.Error()))
}
logger.DebugContext(ctx, "done")
}()
return handle(ctx, r)
}
}
// // Handler wraps the given handle function with logging.
// // The function is safe to call with nil logger.
// func Handler[Req, Res any, H handler.Handle[Req, Res]](logger *logging.Logger, name string, handle H) *handler.Handler[Req, Res, H] {
// return &handler.Handler[Req, Res, H]{
// Handle: Wrap(logger, name, handle),
// }
// }
// // Chained wraps the given handle function with logging.
// // The function is safe to call with nil logger.
// // The next handler is called after the handle function.
// func Chained[Req, Res any, H, N handler.Handle[Req, Res]](logger *logging.Logger, name string, handle H, next N) *handler.Chained[Req, Res, H, N] {
// return handler.NewChained(
// Wrap(logger, name, handle),
// next,
// )
// }

View File

@@ -1,41 +0,0 @@
package logged
import (
"context"
"log/slog"
"github.com/zitadel/zitadel/backend/repository"
"github.com/zitadel/zitadel/backend/telemetry/logging"
)
type Instance struct {
*logging.Logger
next repository.InstanceRepository
}
func NewInstance(logger *logging.Logger, next repository.InstanceRepository) *Instance {
return &Instance{Logger: logger, next: next}
}
var _ repository.InstanceRepository = (*Instance)(nil)
func (i *Instance) ByID(ctx context.Context, id string) (*repository.Instance, error) {
i.Logger.InfoContext(ctx, "By ID Query", slog.String("id", id))
return i.next.ByID(ctx, id)
}
func (i *Instance) ByDomain(ctx context.Context, domain string) (*repository.Instance, error) {
i.Logger.InfoContext(ctx, "By Domain Query", slog.String("domain", domain))
return i.next.ByDomain(ctx, domain)
}
func (i *Instance) SetUp(ctx context.Context, instance *repository.Instance) error {
err := i.next.SetUp(ctx, instance)
if err != nil {
i.Logger.ErrorContext(ctx, "Failed to set up instance", slog.Any("instance", instance), slog.Any("cause", err))
return err
}
i.Logger.InfoContext(ctx, "Instance set up", slog.Any("instance", instance))
return nil
}

View File

@@ -1,36 +0,0 @@
package logged
import (
"context"
"log/slog"
"github.com/zitadel/zitadel/backend/repository"
"github.com/zitadel/zitadel/backend/telemetry/logging"
)
type User struct {
logger *logging.Logger
next repository.UserRepository
}
func NewUser(logger *logging.Logger, next repository.UserRepository) *User {
return &User{logger: logger, next: next}
}
var _ repository.UserRepository = (*User)(nil)
func (i *User) ByID(ctx context.Context, id string) (*repository.User, error) {
i.logger.InfoContext(ctx, "By ID Query", slog.String("id", id))
return i.next.ByID(ctx, id)
}
func (i *User) Create(ctx context.Context, user *repository.User) error {
err := i.next.Create(ctx, user)
if err != nil {
i.logger.ErrorContext(ctx, "Failed to create user", slog.Any("user", user), slog.Any("cause", err))
return err
}
i.logger.InfoContext(ctx, "User created successfully", slog.Any("user", user))
return nil
}

View File

@@ -3,53 +3,75 @@ package traced
import (
"context"
"github.com/zitadel/zitadel/backend/domain/factory"
"github.com/zitadel/zitadel/backend/repository/orchestrate/handler"
"github.com/zitadel/zitadel/backend/telemetry/tracing"
)
type Tracer[Req, Res any] struct {
tracing.Tracer
next factory.Handler[Req, Res]
}
func (*Tracer[Req, Res]) Name() string {
return "Tracer"
}
// Handle implements [factory.Handler].
func (t *Tracer[Req, Res]) Handle(ctx context.Context, request Req) (res Res, err error) {
if t.next == nil {
return res, nil
// 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.Handle[Req, Res]) handler.Handle[Req, Res] {
if tracer == nil {
return handle
}
ctx, span := t.Tracer.Start(
ctx,
t.next.Name(),
)
defer func() {
if err != nil {
span.RecordError(err)
return func(ctx context.Context, r Req) (_ Res, err error) {
ctx, span := tracer.Start(
ctx,
name,
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
return handle(ctx, r)
}
}
func WrapInside(tracer *tracing.Tracer, name string) func(ctx context.Context, fn func() error) {
return func(ctx context.Context, fn func() error) {
var err error
_, span := tracer.Start(
ctx,
name,
)
defer func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
err = fn()
}
}
func DecorateHandle[Req, Res any](tracer *tracing.Tracer, opts ...tracing.DecorateOption) handler.Decorate[Req, Res] {
return func(ctx context.Context, r Req, handle handler.Handle[Req, Res]) (_ Res, err error) {
o := new(tracing.DecorateOptions)
for _, opt := range opts {
opt(o)
}
span.End()
}()
return t.next.Handle(ctx, request)
ctx = o.Start(ctx, tracer)
defer o.End(err)
return handle(ctx, r)
}
}
// SetNext implements [factory.Handler].
func (t *Tracer[Req, Res]) SetNext(next factory.Handler[Req, Res]) {
t.next = next
}
// // Handler wraps the given handle function with tracing.
// // The function is safe to call with nil logger.
// func Handler[Req, Res any, H handler.Handle[Req, Res]](tracer *tracing.Tracer, name string, handle H) *handler.Handler[Req, Res, H] {
// return &handler.Handler[Req, Res, H]{
// Handle: Wrap(tracer, name, handle),
// }
// }
// New implements [factory.Middleware].
func (t *Tracer[Req, Res]) New() factory.Handler[Req, Res] {
return t.NewWithNext(nil)
}
// NewWithNext implements [factory.Middleware].
func (t *Tracer[Req, Res]) NewWithNext(next factory.Handler[Req, Res]) factory.Handler[Req, Res] {
return &Tracer[Req, Res]{Tracer: t.Tracer, next: next}
}
var (
_ factory.Middleware[any, any] = (*Tracer[any, any])(nil)
_ factory.Handler[any, any] = (*Tracer[any, any])(nil)
)
// // Chained wraps the given handle function with tracing.
// // The function is safe to call with nil logger.
// // The next handler is called after the handle function.
// func Chained[Req, Res any, H, N handler.Handle[Req, Res]](tracer *tracing.Tracer, name string, handle H, next N) *handler.Chained[Req, Res, H, N] {
// return handler.NewChained(
// Wrap(tracer, name, handle),
// next,
// )
// }

View File

@@ -1,54 +0,0 @@
package traced
import (
"context"
"github.com/zitadel/zitadel/backend/repository"
"github.com/zitadel/zitadel/backend/telemetry/tracing"
)
var _ repository.InstanceRepository = (*Instance)(nil)
type Instance struct {
*tracing.Tracer
next repository.InstanceRepository
}
func NewInstance(tracer *tracing.Tracer, next repository.InstanceRepository) *Instance {
return &Instance{Tracer: tracer, next: next}
}
func (i *Instance) SetNext(next repository.InstanceRepository) *Instance {
return &Instance{Tracer: i.Tracer, next: next}
}
// ByDomain implements [repository.InstanceRepository].
func (i *Instance) ByDomain(ctx context.Context, domain string) (instance *repository.Instance, err error) {
i.Tracer.Decorate(ctx, func(ctx context.Context) error {
instance, err = i.next.ByDomain(ctx, domain)
return err
})
return instance, err
}
// ByID implements [repository.InstanceRepository].
func (i *Instance) ByID(ctx context.Context, id string) (instance *repository.Instance, err error) {
i.Tracer.Decorate(ctx, func(ctx context.Context) error {
instance, err = i.next.ByID(ctx, id)
return err
})
return instance, err
}
// SetUp implements [repository.InstanceRepository].
func (i *Instance) SetUp(ctx context.Context, instance *repository.Instance) (err error) {
i.Tracer.Decorate(ctx, func(ctx context.Context) error {
err = i.next.SetUp(ctx, instance)
return err
})
return err
}

View File

@@ -1,44 +0,0 @@
package traced
import (
"context"
"github.com/zitadel/zitadel/backend/repository"
"github.com/zitadel/zitadel/backend/telemetry/tracing"
)
var _ repository.UserRepository = (*User)(nil)
type User struct {
*tracing.Tracer
next repository.UserRepository
}
func NewUser(tracer *tracing.Tracer, next repository.UserRepository) *User {
return &User{Tracer: tracer, next: next}
}
func (i *User) SetNext(next repository.UserRepository) *User {
return &User{Tracer: i.Tracer, next: next}
}
// ByID implements [repository.UserRepository].
func (i *User) ByID(ctx context.Context, id string) (user *repository.User, err error) {
i.Tracer.Decorate(ctx, func(ctx context.Context) error {
user, err = i.next.ByID(ctx, id)
return err
})
return user, err
}
// Create implements [repository.UserRepository].
func (i *User) Create(ctx context.Context, user *repository.User) (err error) {
i.Tracer.Decorate(ctx, func(ctx context.Context) error {
err = i.next.Create(ctx, user)
return err
})
return err
}