commit 3ccb7519ef921a9f64f78b02afb593a00f5fdf58 Author: igor.grischenko Date: Mon Mar 16 18:06:59 2026 +0300 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf0824e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.log \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..658a677 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +run_usersUpdateLoginMethod: + go run cmd/main.go -c usersUpdateLoginMethod \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..fc16869 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "flag" + + "scripts/internal/command" + "scripts/internal/config" + e "scripts/pkg/errors" + "scripts/pkg/logger" + l "scripts/pkg/logger" +) + +const defaultConfigPath = "config.json" + +func main() { + var ( + commandName string + confPath string + ) + + flag.StringVar(&confPath, "config", defaultConfigPath, "path to config file") + flag.StringVar(&commandName, "c", "", "command name") + flag.Parse() + + config.MustLoad(confPath) + + if commandName == "" { + l.Log.Fatal().Msg("flag '-c' required") + } + + logger.MustInit() + command.InitSripts() + + cmd, exists := command.Scripts[commandName] + if !exists { + l.Log.Warn().Err(e.ErrCommandNotFound).Str("command", commandName).Send() + l.Log.Info().Msg("selected command - 'help'") + if err := command.Scripts["help"].Exec(); err != nil { + l.Log.Fatal().Err(err).Str("command", commandName).Msg("failed to use help command") + } + return + } + + l.Log.Info().Msgf("selected command - %s", commandName) + if err := cmd.Exec(); err != nil { + l.Log.Error().Err(err).Str("command", commandName).Msg("failed to execude command") + return + } +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..a4c22e7 --- /dev/null +++ b/config.json @@ -0,0 +1,43 @@ +{ + "SqlSettings": { + "DriverName": "postgres", + "DataSource": "postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable\u0026connect_timeout=10\u0026binary_parameters=yes", + "DataSourceReplicas": [], + "DataSourceSearchReplicas": [], + "MaxIdleConns": 20, + "ConnMaxLifetimeMilliseconds": 3600000, + "ConnMaxIdleTimeMilliseconds": 300000, + "MaxOpenConns": 300, + "Trace": false, + "AtRestEncryptKey": "cirgp8x5eydrr9sjmwawb5zsefhbditq", + "QueryTimeout": 30, + "DisableDatabaseSearch": false, + "MigrationsStatementTimeoutSeconds": 100000, + "ReplicaLagSettings": [], + "ReplicaMonitorIntervalSeconds": 5 + }, + "LogSettings": { + "EnableConsole": true, + "ConsoleLevel": "DEBUG", + "ConsoleJson": true, + "EnableColor": false, + "EnableFile": true, + "FileLevel": "INFO", + "FileJson": true, + "FileLocation": "./pkg/logger/logs.log", + "EnableWebhookDebugging": true, + "EnableDiagnostics": true, + "VerboseDiagnostics": false, + "EnableSentry": true, + "EnableCaller": true, + "AdvancedLoggingJSON": {}, + "AdvancedLoggingConfig": "", + "MaxFieldSize": 2048 + }, + "KeycloakSettings": { + "address": "http://localhost:8282", + "realm_name": "loop", + "admin_name": "admin", + "admin_pass": "admin" + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1abcedf --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module scripts + +go 1.26.0 + +require ( + github.com/jmoiron/sqlx v1.4.0 + github.com/lib/pq v1.10.9 + github.com/rs/zerolog v1.34.0 +) + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + golang.org/x/sys v0.12.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..149e51d --- /dev/null +++ b/go.sum @@ -0,0 +1,25 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/command/common.go b/internal/command/common.go new file mode 100644 index 0000000..4bbfc2f --- /dev/null +++ b/internal/command/common.go @@ -0,0 +1,48 @@ +package command + +import ( + "fmt" + commandEmailPassToOidc "scripts/internal/command/email_pass_to_oidc" +) + +func InitSripts() { + scripts := map[string]*Commands{ + commandEmailPassToOidc.Name: { + Exec: commandEmailPassToOidc.UsersUpdateLoginMethod, + Requirements: commandEmailPassToOidc.Requirement, + Description: "переводит авторизацию пользователей с ролью 'system_user' c почта/пароль на keycloak", + }, + "help": { + Exec: Help, + Requirements: Requirement, + Description: "вывод всех доступных скриптов", + }, + } + + Scripts = scripts +} + +var Scripts map[string]*Commands + +type Commands struct { + Exec Execute + Requirements Requirements + Description string +} + +type Execute func() error +type Requirements func() string + +func Help() error { + fmt.Printf("All commands:\n\n") + for k, v := range Scripts { + fmt.Printf("name: %s\n", k) + fmt.Printf("description: %s\n", v.Description) + fmt.Printf("requirements: %s\n\n", v.Requirements()) + } + return nil +} + +func Requirement() string { + return "none" +} diff --git a/internal/command/email_pass_to_oidc/dto.go b/internal/command/email_pass_to_oidc/dto.go new file mode 100644 index 0000000..d49315a --- /dev/null +++ b/internal/command/email_pass_to_oidc/dto.go @@ -0,0 +1,32 @@ +package commandEmailPassToOidc + +type KeycloakUser struct { + ID string `json:"id"` + Username string `json:"username"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Email string `json:"email"` + EmailVerified bool `json:"emailVerified"` + Attributes map[string][]string `json:"attributes"` + Enabled bool `json:"enabled"` +} + +type KeycloakTokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` +} + +type LoopUser struct { + Id string `json:"id" db:"id"` + Username string `json:"username" db:"username"` + Password string `json:"password,omitempty" db:"password"` + AuthData *string `json:"auth_data,omitempty" db:"authdata"` + AuthService *string `json:"auth_service" db:"authservice"` + Email string `json:"email" db:"email"` + EmailVerified bool `json:"email_verified,omitempty" db:"emailverified"` + Nickname string `json:"nickname" db:"nickname"` + FirstName string `json:"first_name" db:"firstname"` + LastName string `json:"last_name" db:"lastname"` + Roles string `json:"roles" db:"roles"` +} diff --git a/internal/command/email_pass_to_oidc/exec.go b/internal/command/email_pass_to_oidc/exec.go new file mode 100644 index 0000000..27cc0d4 --- /dev/null +++ b/internal/command/email_pass_to_oidc/exec.go @@ -0,0 +1,154 @@ +package commandEmailPassToOidc + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + c "scripts/internal/config" + l "scripts/pkg/logger" + "scripts/pkg/postgres" +) + +const Name = "usersUpdateLoginMethod" + +func UsersUpdateLoginMethod() error { + postgres.InitDB() + defer postgres.DB.Close() + + tokenData, err := keycloakAccessToken() + if err != nil { + l.Log.Error().Err(err).Msg("failed to get access token from keycloak") + return err + } + + loopUsers, err := allLoopUsers() + if err != nil { + l.Log.Error().Err(err).Msg("failed to get users from loop db") + return err + } + + if len(loopUsers) == 0 { + l.Log.Info().Msg("no users in loop db for change login") + return nil + } + + keycloakUsers, err := allKeycloakUsers(tokenData.AccessToken) + if err != nil { + l.Log.Error().Err(err).Msg("failed to get users from kecloak") + return err + } + + if err := changeLogin(loopUsers, keycloakUsers); err != nil { + return err + } + + l.Log.Info().Str("name", Name).Msg("command successfully") + return nil +} + +func keycloakAccessToken() (*KeycloakTokenResponse, error) { + url := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/token", c.Conf.KeycloakSettings.Address, "master") + + var data bytes.Buffer + fmt.Fprintf(&data, "grant_type=password&client_id=admin-cli&username=%s&password=%s", c.Conf.KeycloakSettings.AdminName, c.Conf.KeycloakSettings.AdminPass) + + req, err := http.NewRequest("POST", url, &data) + if err != nil { + l.Log.Error().Err(err).Msg("failed to create http request") + return nil, err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + l.Log.Error().Err(err).Msg("failed to exec http request") + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get access token, status %d", resp.StatusCode) + } + + var tokenResp KeycloakTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + l.Log.Error().Err(err).Msg("failed to decode body response") + return nil, err + } + + return &tokenResp, nil +} + +func allLoopUsers() (map[string]LoopUser, error) { + ctx := postgres.DB.Context() + var users []LoopUser + + if err := postgres.DB.Conn().SelectContext(ctx, &users, allUsersQuery); err != nil { + l.Log.Error().Err(err).Msg("failed to select query") + return nil, err + } + + result := make(map[string]LoopUser, len(users)) + for _, el := range users { + result[el.Email] = el + } + + return result, nil +} + +func allKeycloakUsers(accessToken string) ([]KeycloakUser, error) { + url := fmt.Sprintf("%s/admin/realms/%s/users", c.Conf.KeycloakSettings.Address, c.Conf.KeycloakSettings.RealmName) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + l.Log.Error().Err(err).Msg("failed to create http request") + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+accessToken) + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + l.Log.Error().Err(err).Msg("failed to exec http request") + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get users: status %d", resp.StatusCode) + } + + var users []KeycloakUser + if err := json.NewDecoder(resp.Body).Decode(&users); err != nil { + l.Log.Error().Err(err).Msg("failed to decode body response") + return nil, err + } + + return users, nil +} + +func changeLogin(loopUsers map[string]LoopUser, keycloakUsers []KeycloakUser) error { + + for _, el := range keycloakUsers { + + user, exists := loopUsers[el.Email] + if !exists { + l.Log.Warn().Str("email", el.Email).Msg("user not found") + continue + } + + // NOTE: если в keycloak не были указаны firstname и lastname - при первои входе сам keycloak спросит их и ЗАМЕНИТ данные в БД loop + if _, err := postgres.DB.Conn().ExecContext(postgres.DB.Context(), changeLoginQuery, user.Id, el.ID, el.EmailVerified); err != nil { + l.Log.Error().Err(err).Msg("failed to exec update query") + return err + } + + l.Log.Info().Str("email", el.Email).Msg("successfully update") + } + + return nil +} diff --git a/internal/command/email_pass_to_oidc/query.go b/internal/command/email_pass_to_oidc/query.go new file mode 100644 index 0000000..8193152 --- /dev/null +++ b/internal/command/email_pass_to_oidc/query.go @@ -0,0 +1,34 @@ +package commandEmailPassToOidc + +const ( + allUsersQuery = ` + SELECT + users.id, + users.username, + users.password, + users.authdata, + users.authservice, + users.email, + users.emailverified, + users.nickname, + users.firstname, + users.lastname, + users.roles + FROM users + LEFT JOIN bots ON users.id = bots.userid + WHERE users.deleteat = 0 + AND bots.userid IS NULL + AND roles LIKE '%system_user%' + AND roles NOT LIKE '%system_admin%' + AND (users.authservice != 'openid' OR users.authservice IS NULL) + ` + + changeLoginQuery = ` + UPDATE users + SET password = '', + authdata = $2, + emailverified = $3, + authservice = 'openid' + WHERE id = $1 + ` +) diff --git a/internal/command/email_pass_to_oidc/requirement.go b/internal/command/email_pass_to_oidc/requirement.go new file mode 100644 index 0000000..7266478 --- /dev/null +++ b/internal/command/email_pass_to_oidc/requirement.go @@ -0,0 +1,20 @@ +package commandEmailPassToOidc + +import ( + "reflect" + c "scripts/internal/config" + "strings" +) + +func Requirement() string { + var required []string + + t := reflect.TypeFor[c.KeycloakSettings]() + + for field := range t.Fields() { + jsonTag := field.Tag.Get("json") + required = append(required, jsonTag) + } + + return "необходимо задать в конфигурации ключ KeycloakSettings с ключами в объекте: " + strings.Join(required, "; ") +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..d041ce2 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,67 @@ +package config + +import ( + "encoding/json" + "log" + "os" +) + +type SqlSettings struct { + DriverName string `json:"DriverName"` + DataSource string `json:"DataSource"` + DataSourceReplicas []string `json:"DataSourceReplicas"` + DataSourceSearchReplicas []string `json:"DataSourceSearchReplicas"` + MaxIdleConns int `json:"MaxIdleConns"` + ConnMaxLifetimeMilliseconds int64 `json:"ConnMaxLifetimeMilliseconds"` + ConnMaxIdleTimeMilliseconds int64 `json:"ConnMaxIdleTimeMilliseconds"` + MaxOpenConns int `json:"MaxOpenConns"` + Trace bool `json:"Trace"` + AtRestEncryptKey string `json:"AtRestEncryptKey"` + QueryTimeout int `json:"QueryTimeout"` + DisableDatabaseSearch bool `json:"DisableDatabaseSearch"` + MigrationsStatementTimeoutSeconds int `json:"MigrationsStatementTimeoutSeconds"` + ReplicaMonitorIntervalSeconds int `json:"ReplicaMonitorIntervalSeconds"` + // ReplicaLagSettings []ReplicaLagSetting `json:"ReplicaLagSettings"` +} + +type LogSettings struct { + EnableConsole bool `json:"EnableConsole"` + ConsoleLevel string `json:"ConsoleLevel"` + ConsoleJson bool `json:"ConsoleJson"` + EnableColor bool `json:"EnableColor"` + EnableFile bool `json:"EnableFile"` + FileLevel string `json:"FileLevel"` + FileJson bool `json:"FileJson"` + FileLocation string `json:"FileLocation"` + EnableWebhookDebugging bool `json:"EnableWebhookDebugging"` + EnableDiagnostics bool `json:"EnableDiagnostics"` + VerboseDiagnostics bool `json:"VerboseDiagnostics"` + EnableSentry bool `json:"EnableSentry"` + EnableCaller bool `json:"EnableCaller"` + AdvancedLoggingConfig string `json:"AdvancedLoggingConfig"` + MaxFieldSize int `json:"MaxFieldSize"` + // AdvancedLoggingJSON map[string]interface{} `json:"AdvancedLoggingJSON"` +} + +var Conf Config + +type Config struct { + SqlSettings SqlSettings `json:"SqlSettings"` + LogSettings LogSettings `json:"LogSettings"` + KeycloakSettings KeycloakSettings `json:"KeycloakSettings"` +} + +func MustLoad(confPath string) { + file, err := os.Open(confPath) + if err != nil { + log.Fatal("err", "failed to open file") + } + defer file.Close() + + decoder := json.NewDecoder(file) + if err := decoder.Decode(&Conf); err != nil { + log.Fatal(err, "failed to decode") + } + + log.Println("init config successfully") +} diff --git a/internal/config/email_pass_to_oidc.go b/internal/config/email_pass_to_oidc.go new file mode 100644 index 0000000..647990a --- /dev/null +++ b/internal/config/email_pass_to_oidc.go @@ -0,0 +1,8 @@ +package config + +type KeycloakSettings struct { + Address string `json:"address"` + RealmName string `json:"realm_name"` + AdminName string `json:"admin_name"` + AdminPass string `json:"admin_pass"` +} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 0000000..252b284 --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,7 @@ +package errors + +import "errors" + +var ( + ErrCommandNotFound = errors.New("command not found") +) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..727df8e --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,54 @@ +package logger + +import ( + "io" + "log" + "os" + c "scripts/internal/config" + + "github.com/rs/zerolog" +) + +const timeFormat = "2006-01-02 15:04:05" + +var Log zerolog.Logger + +func MustInit() { + writers := make([]io.Writer, 0, 2) + + zerolog.TimeFieldFormat = timeFormat + + consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: timeFormat} + writers = append(writers, consoleWriter) + + if c.Conf.LogSettings.EnableFile { + logFile, err := os.OpenFile(c.Conf.LogSettings.FileLocation, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + log.Fatal(err) + } + writers = append(writers, logFile) + } + + multi := zerolog.MultiLevelWriter(writers...) + zCtx := zerolog.New(multi).Level(detectedLogLevel(c.Conf.LogSettings.ConsoleLevel)).With().Timestamp() + if c.Conf.LogSettings.EnableCaller { + zCtx = zCtx.Caller() + } + + Log = zCtx.Logger() +} + +func detectedLogLevel(level string) zerolog.Level { + switch level { + case "TRACE": + return zerolog.TraceLevel + case "DEBUG": + return zerolog.DebugLevel + case "WARN": + return zerolog.WarnLevel + case "INFO": + return zerolog.InfoLevel + default: + return zerolog.InfoLevel + } +} diff --git a/pkg/postgres/connect.go b/pkg/postgres/connect.go new file mode 100644 index 0000000..530a03c --- /dev/null +++ b/pkg/postgres/connect.go @@ -0,0 +1,68 @@ +package postgres + +import ( + "context" + c "scripts/internal/config" + l "scripts/pkg/logger" + "time" + + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" +) + +var DB *Postgres + +type Option func(*Postgres) + +type Postgres struct { + ctx context.Context + conn *sqlx.DB +} + +func (p *Postgres) Conn() *sqlx.DB { + return p.conn +} + +func (p *Postgres) Context() context.Context { + return p.ctx +} + +func WithContext(ctx context.Context) Option { + return func(p *Postgres) { + p.ctx = ctx + } +} + +func InitDB(opts ...Option) { + p := &Postgres{ + ctx: context.Background(), + } + + for _, opt := range opts { + opt(p) + } + + db, err := sqlx.ConnectContext(p.ctx, c.Conf.SqlSettings.DriverName, c.Conf.SqlSettings.DataSource) + if err != nil { + l.Log.Fatal().Err(err).Msg("failed to connect DB") + } + + db.SetMaxIdleConns(c.Conf.SqlSettings.MaxIdleConns) + db.SetMaxOpenConns(c.Conf.SqlSettings.MaxOpenConns) + db.SetConnMaxLifetime(time.Duration(c.Conf.SqlSettings.ConnMaxLifetimeMilliseconds) * time.Millisecond) + db.SetConnMaxIdleTime(time.Duration(c.Conf.SqlSettings.ConnMaxIdleTimeMilliseconds) * time.Millisecond) + + if err := db.Ping(); err != nil { + l.Log.Fatal().Err(err).Msg("failed to ping DB") + } + + p.conn = db + DB = p + l.Log.Info().Msg("connect to postgres successfully ") +} + +func (p *Postgres) Close() { + if err := p.Conn().Close(); err != nil { + l.Log.Error().Err(err).Msg("failed to close connect DB") + } +}