From e778ec2edf25aa861dc150444028bfebc8070caa Mon Sep 17 00:00:00 2001 From: "stonezdj(Daojun Zhang)" Date: Fri, 10 Jun 2022 10:59:40 +0800 Subject: [PATCH] Add audit_log forward endpoint (#16914) add config item to set log forward endpoint fallback the audit log to default log when endpoint in error Signed-off-by: stonezdj --- api/v2.0/swagger.yaml | 16 ++ src/common/const.go | 4 + src/controller/config/controller.go | 32 ++++ src/controller/config/controller_test.go | 59 +++++++ .../event/handler/auditlog/auditlog_test.go | 1 + src/core/main.go | 24 +-- src/lib/config/metadata/metadatalist.go | 3 + src/lib/config/userconfig.go | 10 ++ src/lib/log/logger.go | 41 +++-- src/lib/log/logger_test.go | 6 +- src/pkg/audit/forward.go | 74 +++++++++ src/pkg/audit/manager.go | 9 + src/pkg/audit/manager_test.go | 1 + src/testing/lib/config/manager.go | 157 ++++++++++++++++++ src/testing/lib/lib.go | 1 + 15 files changed, 410 insertions(+), 28 deletions(-) create mode 100644 src/controller/config/controller_test.go create mode 100644 src/pkg/audit/forward.go create mode 100644 src/testing/lib/config/manager.go diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 5c717350e..35501d002 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -8378,6 +8378,12 @@ definitions: storage_per_project: $ref: '#/definitions/IntegerConfigItem' description: The storage quota per project + audit_log_forward_endpoint: + $ref: '#/definitions/StringConfigItem' + description: The endpoint of the audit log forwarder + skip_audit_log_database: + $ref: '#/definitions/BoolConfigItem' + description: Whether skip the audit log in database scan_all_policy: type: object properties: @@ -8669,6 +8675,16 @@ definitions: description: The storage quota per project x-omitempty: true x-isnullable: true + audit_log_forward_endpoint: + type: string + description: The audit log forward endpoint + x-omitempty: true + x-isnullable: true + skip_audit_log_database: + type: boolean + description: Skip audit log database + x-omitempty: true + x-isnullable: true StringConfigItem: type: object properties: diff --git a/src/common/const.go b/src/common/const.go index 4caafc380..7f213b484 100755 --- a/src/common/const.go +++ b/src/common/const.go @@ -209,4 +209,8 @@ const ( PurgeAuditIncludeOperations = "include_operations" PurgeAuditDryRun = "dry_run" PurgeAuditRetentionHour = "audit_retention_hour" + // AuditLogForwardEndpoint indicate to forward the audit log to an endpoint + AuditLogForwardEndpoint = "audit_log_forward_endpoint" + // SkipAuditLogDatabase skip to log audit log in database + SkipAuditLogDatabase = "skip_audit_log_database" ) diff --git a/src/controller/config/controller.go b/src/controller/config/controller.go index 2635c9ed7..3cebe9240 100644 --- a/src/controller/config/controller.go +++ b/src/controller/config/controller.go @@ -27,6 +27,7 @@ import ( "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/audit" "github.com/goharbor/harbor/src/pkg/user" ) @@ -92,6 +93,22 @@ func (c *controller) UpdateUserConfigs(ctx context.Context, conf map[string]inte log.Errorf("failed to upload configurations: %v", err) return fmt.Errorf("failed to validate configuration") } + // update the audit logger to point to the new endpoint + return c.updateLogEndpoint(ctx, conf) +} + +func (c *controller) updateLogEndpoint(ctx context.Context, cfgs map[string]interface{}) error { + // check if the audit log forward endpoint updated + if _, ok := cfgs[common.AuditLogForwardEndpoint]; ok { + auditEP := config.AuditLogForwardEndpoint(ctx) + if len(auditEP) == 0 { + return nil + } + if !audit.CheckEndpointActive(auditEP) { + return errors.BadRequestError(fmt.Errorf("could not connect to the audit endpoint: %v", auditEP)) + } + audit.LogMgr.Init(ctx, auditEP) + } return nil } @@ -116,6 +133,21 @@ func (c *controller) validateCfg(ctx context.Context, cfgs map[string]interface{ if err != nil { return errors.BadRequestError(err) } + + return verifySkipAuditLogCfg(ctx, cfgs, mgr) +} + +func verifySkipAuditLogCfg(ctx context.Context, cfgs map[string]interface{}, mgr config.Manager) error { + if skip, exist := cfgs[common.SkipAuditLogDatabase]; exist { + endPoint := mgr.Get(ctx, common.AuditLogForwardEndpoint).GetString() + if edp, found := cfgs[common.AuditLogForwardEndpoint]; found { + endPoint = edp.(string) + } + skipAuditDB := skip.(bool) + if len(endPoint) == 0 && skipAuditDB { + return errors.BadRequestError(errors.New("audit log forward endpoint should be configured before enable skip audit log in database")) + } + } return nil } diff --git a/src/controller/config/controller_test.go b/src/controller/config/controller_test.go new file mode 100644 index 000000000..a0f6f245e --- /dev/null +++ b/src/controller/config/controller_test.go @@ -0,0 +1,59 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "context" + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/lib/config" + "github.com/goharbor/harbor/src/lib/config/metadata" + testCfg "github.com/goharbor/harbor/src/testing/lib/config" + "github.com/goharbor/harbor/src/testing/mock" + "testing" +) + +func Test_verifySkipAuditLogCfg(t *testing.T) { + cfgManager := &testCfg.Manager{} + cfgManager.On("Get", mock.Anything, common.AuditLogForwardEndpoint). + Return(&metadata.ConfigureValue{Name: common.AuditLogForwardEndpoint, Value: ""}) + type args struct { + ctx context.Context + cfgs map[string]interface{} + mgr config.Manager + } + tests := []struct { + name string + args args + wantErr bool + }{ + {name: "both configured", args: args{ctx: context.TODO(), + cfgs: map[string]interface{}{common.AuditLogForwardEndpoint: "harbor-log:15041", + common.SkipAuditLogDatabase: true}, + mgr: cfgManager}, wantErr: false}, + {name: "no forward endpoint config", args: args{ctx: context.TODO(), + cfgs: map[string]interface{}{common.SkipAuditLogDatabase: true}, + mgr: cfgManager}, wantErr: true}, + {name: "none configured", args: args{ctx: context.TODO(), + cfgs: map[string]interface{}{}, + mgr: cfgManager}, wantErr: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := verifySkipAuditLogCfg(tt.args.ctx, tt.args.cfgs, tt.args.mgr); (err != nil) != tt.wantErr { + t.Errorf("verifySkipAuditLogCfg() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/src/controller/event/handler/auditlog/auditlog_test.go b/src/controller/event/handler/auditlog/auditlog_test.go index 24eb3a81a..017777226 100644 --- a/src/controller/event/handler/auditlog/auditlog_test.go +++ b/src/controller/event/handler/auditlog/auditlog_test.go @@ -23,6 +23,7 @@ import ( "github.com/goharbor/harbor/src/controller/event/metadata" "github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/pkg/audit/model" + _ "github.com/goharbor/harbor/src/pkg/config/db" "github.com/goharbor/harbor/src/pkg/notifier" ne "github.com/goharbor/harbor/src/pkg/notifier/event" "github.com/stretchr/testify/mock" diff --git a/src/core/main.go b/src/core/main.go index ec14435de..00c2a8d08 100755 --- a/src/core/main.go +++ b/src/core/main.go @@ -1,16 +1,16 @@ -// Copyright 2018 Project Harbor Authors +// Copyright Project Harbor Authors // -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package main @@ -53,6 +53,7 @@ import ( "github.com/goharbor/harbor/src/lib/orm" tracelib "github.com/goharbor/harbor/src/lib/trace" "github.com/goharbor/harbor/src/migration" + "github.com/goharbor/harbor/src/pkg/audit" dbCfg "github.com/goharbor/harbor/src/pkg/config/db" _ "github.com/goharbor/harbor/src/pkg/config/inmemory" "github.com/goharbor/harbor/src/pkg/notification" @@ -202,6 +203,9 @@ func main() { go gracefulShutdown(closing, done, shutdownTracerProvider) // Start health checker for registries go registry.Ctl.StartRegularHealthCheck(orm.Context(), closing, done) + // Init audit log + auditEP := config.AuditLogForwardEndpoint(ctx) + audit.LogMgr.Init(ctx, auditEP) log.Info("initializing notification...") notification.Init() diff --git a/src/lib/config/metadata/metadatalist.go b/src/lib/config/metadata/metadatalist.go index 05d3adce7..12e0d4138 100644 --- a/src/lib/config/metadata/metadatalist.go +++ b/src/lib/config/metadata/metadatalist.go @@ -186,5 +186,8 @@ var ( {Name: common.CacheEnabled, Scope: SystemScope, Group: BasicGroup, EnvKey: "CACHE_ENABLED", DefaultValue: "false", ItemType: &BoolType{}, Editable: false, Description: `Enable cache`}, {Name: common.CacheExpireHours, Scope: SystemScope, Group: BasicGroup, EnvKey: "CACHE_EXPIRE_HOURS", DefaultValue: "24", ItemType: &IntType{}, Editable: false, Description: `The expire hours for cache`}, + + {Name: common.AuditLogForwardEndpoint, Scope: UserScope, Group: BasicGroup, EnvKey: "AUDIT_LOG_FORWARD_ENDPOINT", DefaultValue: "", ItemType: &StringType{}, Editable: false, Description: `The endpoint to forward the audit log.`}, + {Name: common.SkipAuditLogDatabase, Scope: UserScope, Group: BasicGroup, EnvKey: "SKIP_LOG_AUDIT_DATABASE", DefaultValue: "false", ItemType: &BoolType{}, Editable: false, Description: `The option to skip audit log in database`}, } ) diff --git a/src/lib/config/userconfig.go b/src/lib/config/userconfig.go index a80502323..7baaa5b15 100644 --- a/src/lib/config/userconfig.go +++ b/src/lib/config/userconfig.go @@ -241,3 +241,13 @@ func PullTimeUpdateDisable(ctx context.Context) bool { func PullAuditLogDisable(ctx context.Context) bool { return DefaultMgr().Get(ctx, common.PullAuditLogDisable).GetBool() } + +// AuditLogForwardEndpoint returns the audit log forward endpoint +func AuditLogForwardEndpoint(ctx context.Context) string { + return DefaultMgr().Get(ctx, common.AuditLogForwardEndpoint).GetString() +} + +// SkipAuditLogDatabase returns the audit log forward endpoint +func SkipAuditLogDatabase(ctx context.Context) bool { + return DefaultMgr().Get(ctx, common.SkipAuditLogDatabase).GetBool() +} diff --git a/src/lib/log/logger.go b/src/lib/log/logger.go index a3d0fe8dd..04bd1b2b9 100644 --- a/src/lib/log/logger.go +++ b/src/lib/log/logger.go @@ -1,16 +1,16 @@ -// Copyright Project Harbor Authors +// Copyright Project Harbor Authors // -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package log @@ -59,6 +59,7 @@ type Logger struct { fields map[string]interface{} fieldsStr string mu *sync.Mutex // ptr here to share one sync.Mutex for clone method + fallback *Logger // fallback logger when current out fail } // New returns a customized Logger @@ -89,6 +90,11 @@ func DefaultLogger() *Logger { return logger } +// SetFallback enable fallback when error happen +func (l *Logger) SetFallback(logger *Logger) { + l.fallback = logger +} + func (l *Logger) clone() *Logger { return &Logger{ out: l.out, @@ -146,8 +152,8 @@ func (l *Logger) WithField(key string, value interface{}) *Logger { return l.WithFields(Fields{key: value}) } -// setOutput sets the output of Logger l -func (l *Logger) setOutput(out io.Writer) { +// SetOutput sets the output of Logger l +func (l *Logger) SetOutput(out io.Writer) { l.mu.Lock() defer l.mu.Unlock() @@ -175,12 +181,17 @@ func (l *Logger) output(record *Record) (err error) { if err != nil { return } - + defer func() { + if err := recover(); err != nil && l.fallback != nil { + _ = l.fallback.output(record) + } + }() l.mu.Lock() defer l.mu.Unlock() - _, err = l.out.Write(b) - + if err != nil && l.fallback != nil { + _ = l.fallback.output(record) + } return } diff --git a/src/lib/log/logger_test.go b/src/lib/log/logger_test.go index 4f8933d77..502abc76c 100644 --- a/src/lib/log/logger_test.go +++ b/src/lib/log/logger_test.go @@ -32,7 +32,7 @@ func contains(t *testing.T, str string, lvl string, line, msg string) bool { func TestSetx(t *testing.T) { logger := New(nil, nil, WarningLevel) - logger.setOutput(os.Stdout) + logger.SetOutput(os.Stdout) fmt := NewTextFormatter() logger.setFormatter(fmt) logger.setLevel(DebugLevel) @@ -223,11 +223,11 @@ func enter() *bytes.Buffer { b := make([]byte, 0, 32) buf := bytes.NewBuffer(b) - logger.setOutput(buf) + logger.SetOutput(buf) return buf } func exit() { - logger.setOutput(os.Stdout) + logger.SetOutput(os.Stdout) } diff --git a/src/pkg/audit/forward.go b/src/pkg/audit/forward.go new file mode 100644 index 000000000..efc6b0041 --- /dev/null +++ b/src/pkg/audit/forward.go @@ -0,0 +1,74 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package audit + +import ( + "context" + "github.com/goharbor/harbor/src/lib/config" + "github.com/goharbor/harbor/src/lib/log" + "io" + "log/syslog" + "os" +) + +// LogMgr manage the audit log forward operations +var LogMgr = &LoggerManager{} + +// LoggerManager manage the operations related to the audit log +type LoggerManager struct { + endpoint string + initialized bool + remoteLogger *log.Logger +} + +// Init redirect the audit log to the forward endpoint +func (a *LoggerManager) Init(ctx context.Context, logEndpoint string) { + var w io.Writer + w, err := syslog.Dial("tcp", logEndpoint, + syslog.LOG_INFO, "audit") + a.initialized = true + if err != nil { + log.Errorf("failed to create audit log, error %v", err) + w = os.Stdout + a.initialized = false + } + a.remoteLogger = log.New(w, log.NewTextFormatter(), log.InfoLevel, 3) + a.remoteLogger.SetFallback(log.DefaultLogger()) + +} + +// DefaultLogger ... +func (a *LoggerManager) DefaultLogger(ctx context.Context) *log.Logger { + endpoint := config.AuditLogForwardEndpoint(ctx) + if a.endpoint != endpoint { + a.Init(ctx, endpoint) + a.initialized = true + } + return a.remoteLogger +} + +// CheckEndpointActive check the liveliness of the endpoint +func CheckEndpointActive(address string) bool { + al, err := syslog.Dial("tcp", address, + syslog.LOG_INFO, "audit") + if al != nil { + defer al.Close() + } + if err != nil { + log.Errorf("failed to connect to audit log endpoint, error %v", err) + return false + } + return true +} diff --git a/src/pkg/audit/manager.go b/src/pkg/audit/manager.go index 284ac48d1..65dbb78fa 100644 --- a/src/pkg/audit/manager.go +++ b/src/pkg/audit/manager.go @@ -16,6 +16,7 @@ package audit import ( "context" + "github.com/goharbor/harbor/src/lib/config" "github.com/goharbor/harbor/src/lib/q" "github.com/goharbor/harbor/src/pkg/audit/dao" "github.com/goharbor/harbor/src/pkg/audit/model" @@ -68,6 +69,14 @@ func (m *manager) Get(ctx context.Context, id int64) (*model.AuditLog, error) { // Create ... func (m *manager) Create(ctx context.Context, audit *model.AuditLog) (int64, error) { + if len(config.AuditLogForwardEndpoint(ctx)) > 0 { + LogMgr.DefaultLogger(ctx).WithField("operator", audit.Username). + WithField("time", audit.OpTime). + Infof("action:%s, resource:%s", audit.Operation, audit.Resource) + } + if config.SkipAuditLogDatabase(ctx) { + return 0, nil + } return m.dao.Create(ctx, audit) } diff --git a/src/pkg/audit/manager_test.go b/src/pkg/audit/manager_test.go index 7eee6f795..01dfe6283 100644 --- a/src/pkg/audit/manager_test.go +++ b/src/pkg/audit/manager_test.go @@ -16,6 +16,7 @@ package audit import ( "github.com/goharbor/harbor/src/pkg/audit/model" + _ "github.com/goharbor/harbor/src/pkg/config/db" mockDAO "github.com/goharbor/harbor/src/testing/pkg/audit/dao" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" diff --git a/src/testing/lib/config/manager.go b/src/testing/lib/config/manager.go new file mode 100644 index 000000000..d5182df8c --- /dev/null +++ b/src/testing/lib/config/manager.go @@ -0,0 +1,157 @@ +// Code generated by mockery v2.12.3. DO NOT EDIT. + +package config + +import ( + context "context" + + metadata "github.com/goharbor/harbor/src/lib/config/metadata" + mock "github.com/stretchr/testify/mock" + + models "github.com/goharbor/harbor/src/common/models" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// Get provides a mock function with given fields: ctx, key +func (_m *Manager) Get(ctx context.Context, key string) *metadata.ConfigureValue { + ret := _m.Called(ctx, key) + + var r0 *metadata.ConfigureValue + if rf, ok := ret.Get(0).(func(context.Context, string) *metadata.ConfigureValue); ok { + r0 = rf(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*metadata.ConfigureValue) + } + } + + return r0 +} + +// GetAll provides a mock function with given fields: ctx +func (_m *Manager) GetAll(ctx context.Context) map[string]interface{} { + ret := _m.Called(ctx) + + var r0 map[string]interface{} + if rf, ok := ret.Get(0).(func(context.Context) map[string]interface{}); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + return r0 +} + +// GetDatabaseCfg provides a mock function with given fields: +func (_m *Manager) GetDatabaseCfg() *models.Database { + ret := _m.Called() + + var r0 *models.Database + if rf, ok := ret.Get(0).(func() *models.Database); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Database) + } + } + + return r0 +} + +// GetUserCfgs provides a mock function with given fields: ctx +func (_m *Manager) GetUserCfgs(ctx context.Context) map[string]interface{} { + ret := _m.Called(ctx) + + var r0 map[string]interface{} + if rf, ok := ret.Get(0).(func(context.Context) map[string]interface{}); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + return r0 +} + +// Load provides a mock function with given fields: ctx +func (_m *Manager) Load(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Save provides a mock function with given fields: ctx +func (_m *Manager) Save(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Set provides a mock function with given fields: ctx, key, value +func (_m *Manager) Set(ctx context.Context, key string, value interface{}) { + _m.Called(ctx, key, value) +} + +// UpdateConfig provides a mock function with given fields: ctx, cfgs +func (_m *Manager) UpdateConfig(ctx context.Context, cfgs map[string]interface{}) error { + ret := _m.Called(ctx, cfgs) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, map[string]interface{}) error); ok { + r0 = rf(ctx, cfgs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ValidateCfg provides a mock function with given fields: ctx, cfgs +func (_m *Manager) ValidateCfg(ctx context.Context, cfgs map[string]interface{}) error { + ret := _m.Called(ctx, cfgs) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, map[string]interface{}) error); ok { + r0 = rf(ctx, cfgs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type NewManagerT interface { + mock.TestingT + Cleanup(func()) +} + +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewManager(t NewManagerT) *Manager { + mock := &Manager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/src/testing/lib/lib.go b/src/testing/lib/lib.go index e157037a3..24eff32bd 100644 --- a/src/testing/lib/lib.go +++ b/src/testing/lib/lib.go @@ -16,3 +16,4 @@ package lib //go:generate mockery --case snake --dir ../../lib/orm --name Creator --output ./orm --outpkg orm //go:generate mockery --case snake --dir ../../lib/cache --name Cache --output ./cache --outpkg cache +//go:generate mockery --case snake --dir ../../lib/config --name Manager --output ./config --outpkg config