mirror of https://github.com/1Panel-dev/1Panel
feat: 完成 redis 增删改查
parent
0f136570fe
commit
37dee0dd81
|
@ -0,0 +1,85 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/global"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) SetRedis(c *gin.Context) {
|
||||||
|
var req dto.RedisDataSet
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := redisService.Set(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) SearchRedis(c *gin.Context) {
|
||||||
|
var req dto.SearchRedisWithPage
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total, list, err := redisService.SearchWithPage(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, dto.PageResult{
|
||||||
|
Items: list,
|
||||||
|
Total: total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) DeleteRedis(c *gin.Context) {
|
||||||
|
var req dto.RedisDelBatch
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := redisService.Delete(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) CleanRedis(c *gin.Context) {
|
||||||
|
db, ok := c.Params.Get("db")
|
||||||
|
if !ok {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error db in path"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dbNum, err := strconv.Atoi(db)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error db in path"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := redisService.CleanAll(dbNum); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ var (
|
||||||
imageService = service.ServiceGroupApp.ImageService
|
imageService = service.ServiceGroupApp.ImageService
|
||||||
|
|
||||||
mysqlService = service.ServiceGroupApp.MysqlService
|
mysqlService = service.ServiceGroupApp.MysqlService
|
||||||
|
redisService = service.ServiceGroupApp.RedisService
|
||||||
|
|
||||||
cronjobService = service.ServiceGroupApp.CronjobService
|
cronjobService = service.ServiceGroupApp.CronjobService
|
||||||
|
|
||||||
|
|
|
@ -25,11 +25,12 @@ type BackupSearch struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackupRecords struct {
|
type BackupRecords struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
FileDir string `json:"fileDir"`
|
BackupType string `json:"backupType"`
|
||||||
FileName string `json:"fileName"`
|
FileDir string `json:"fileDir"`
|
||||||
|
FileName string `json:"fileName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloadRecord struct {
|
type DownloadRecord struct {
|
||||||
|
|
|
@ -135,3 +135,29 @@ type RecoverDB struct {
|
||||||
DBName string `json:"dbName" validate:"required"`
|
DBName string `json:"dbName" validate:"required"`
|
||||||
BackupName string `json:"backupName" validate:"required"`
|
BackupName string `json:"backupName" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redis
|
||||||
|
type SearchRedisWithPage struct {
|
||||||
|
PageInfo
|
||||||
|
DB int `json:"db" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedisData struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Length int64 `json:"length"`
|
||||||
|
Expiration int64 `json:"expiration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedisDataSet struct {
|
||||||
|
DB int `json:"db"`
|
||||||
|
Key string `json:"key" validate:"required"`
|
||||||
|
Value string `json:"value" validate:"required"`
|
||||||
|
Expiration int64 `json:"expiration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedisDelBatch struct {
|
||||||
|
DB int `json:"db" validate:"required"`
|
||||||
|
Names []string `json:"names" validate:"required"`
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ type BackupRecord struct {
|
||||||
Name string `gorm:"type:varchar(64);not null" json:"name"`
|
Name string `gorm:"type:varchar(64);not null" json:"name"`
|
||||||
DetailName string `gorm:"type:varchar(256)" json:"detailName"`
|
DetailName string `gorm:"type:varchar(256)" json:"detailName"`
|
||||||
Source string `gorm:"type:varchar(256)" json:"source"`
|
Source string `gorm:"type:varchar(256)" json:"source"`
|
||||||
|
BackupType string `gorm:"type:varchar(256)" json:"backupType"`
|
||||||
FileDir string `gorm:"type:varchar(256)" json:"fileDir"`
|
FileDir string `gorm:"type:varchar(256)" json:"fileDir"`
|
||||||
FileName string `gorm:"type:varchar(256)" json:"fileName"`
|
FileName string `gorm:"type:varchar(256)" json:"fileName"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ type IMysqlRepo interface {
|
||||||
Delete(opts ...DBOption) error
|
Delete(opts ...DBOption) error
|
||||||
Update(id uint, vars map[string]interface{}) error
|
Update(id uint, vars map[string]interface{}) error
|
||||||
LoadRunningVersion() ([]string, error)
|
LoadRunningVersion() ([]string, error)
|
||||||
LoadBaseInfoByVersion(key string) (*RootInfo, error)
|
LoadBaseInfoByKey(key string) (*RootInfo, error)
|
||||||
UpdateMysqlConf(id uint, vars map[string]interface{}) error
|
UpdateMysqlConf(id uint, vars map[string]interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ type RootInfo struct {
|
||||||
Env string `json:"env"`
|
Env string `json:"env"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlRepo) LoadBaseInfoByVersion(key string) (*RootInfo, error) {
|
func (u *MysqlRepo) LoadBaseInfoByKey(key string) (*RootInfo, error) {
|
||||||
var (
|
var (
|
||||||
app model.App
|
app model.App
|
||||||
appInstall model.AppInstall
|
appInstall model.AppInstall
|
||||||
|
|
|
@ -66,7 +66,7 @@ func (u *MysqlService) ListDBByVersion(version string) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) SearchBackupsWithPage(search dto.SearchBackupsWithPage) (int64, interface{}, error) {
|
func (u *MysqlService) SearchBackupsWithPage(search dto.SearchBackupsWithPage) (int64, interface{}, error) {
|
||||||
app, err := mysqlRepo.LoadBaseInfoByVersion(search.Version)
|
app, err := mysqlRepo.LoadBaseInfoByKey(search.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ func (u *MysqlService) Create(mysqlDto dto.MysqlDBCreate) error {
|
||||||
return errors.WithMessage(constant.ErrStructTransform, err.Error())
|
return errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := mysqlRepo.LoadBaseInfoByVersion(mysqlDto.Version)
|
app, err := mysqlRepo.LoadBaseInfoByKey(mysqlDto.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -138,7 +138,7 @@ func (u *MysqlService) Backup(db dto.BackupDB) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) Recover(db dto.RecoverDB) error {
|
func (u *MysqlService) Recover(db dto.RecoverDB) error {
|
||||||
app, err := mysqlRepo.LoadBaseInfoByVersion(db.Version)
|
app, err := mysqlRepo.LoadBaseInfoByKey(db.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ func (u *MysqlService) Recover(db dto.RecoverDB) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) Delete(version string, ids []uint) error {
|
func (u *MysqlService) Delete(version string, ids []uint) error {
|
||||||
app, err := mysqlRepo.LoadBaseInfoByVersion(version)
|
app, err := mysqlRepo.LoadBaseInfoByKey(version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -198,7 +198,7 @@ func (u *MysqlService) ChangeInfo(info dto.ChangeDBInfo) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app, err := mysqlRepo.LoadBaseInfoByVersion(info.Version)
|
app, err := mysqlRepo.LoadBaseInfoByKey(info.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -266,7 +266,7 @@ func (u *MysqlService) ChangeInfo(info dto.ChangeDBInfo) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) UpdateVariables(variables dto.MysqlVariablesUpdate) error {
|
func (u *MysqlService) UpdateVariables(variables dto.MysqlVariablesUpdate) error {
|
||||||
app, err := mysqlRepo.LoadBaseInfoByVersion(variables.Version)
|
app, err := mysqlRepo.LoadBaseInfoByKey(variables.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -320,7 +320,7 @@ func (u *MysqlService) UpdateVariables(variables dto.MysqlVariablesUpdate) error
|
||||||
|
|
||||||
func (u *MysqlService) LoadBaseInfo(version string) (*dto.DBBaseInfo, error) {
|
func (u *MysqlService) LoadBaseInfo(version string) (*dto.DBBaseInfo, error) {
|
||||||
var data dto.DBBaseInfo
|
var data dto.DBBaseInfo
|
||||||
app, err := mysqlRepo.LoadBaseInfoByVersion(version)
|
app, err := mysqlRepo.LoadBaseInfoByKey(version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -342,7 +342,7 @@ func (u *MysqlService) LoadBaseInfo(version string) (*dto.DBBaseInfo, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) LoadVariables(version string) (*dto.MysqlVariables, error) {
|
func (u *MysqlService) LoadVariables(version string) (*dto.MysqlVariables, error) {
|
||||||
app, err := mysqlRepo.LoadBaseInfoByVersion(version)
|
app, err := mysqlRepo.LoadBaseInfoByKey(version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -360,7 +360,7 @@ func (u *MysqlService) LoadVariables(version string) (*dto.MysqlVariables, error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) LoadStatus(version string) (*dto.MysqlStatus, error) {
|
func (u *MysqlService) LoadStatus(version string) (*dto.MysqlStatus, error) {
|
||||||
app, err := mysqlRepo.LoadBaseInfoByVersion(version)
|
app, err := mysqlRepo.LoadBaseInfoByKey(version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -444,7 +444,7 @@ func excuteSql(containerName, password, command string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func backupMysql(backupType, baseDir, backupDir, version, dbName, fileName string) error {
|
func backupMysql(backupType, baseDir, backupDir, version, dbName, fileName string) error {
|
||||||
app, err := mysqlRepo.LoadBaseInfoByVersion(version)
|
app, err := mysqlRepo.LoadBaseInfoByKey(version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -471,6 +471,7 @@ func backupMysql(backupType, baseDir, backupDir, version, dbName, fileName strin
|
||||||
Name: app.Name,
|
Name: app.Name,
|
||||||
DetailName: dbName,
|
DetailName: dbName,
|
||||||
Source: backupType,
|
Source: backupType,
|
||||||
|
BackupType: backupType,
|
||||||
FileDir: backupDir,
|
FileDir: backupDir,
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||||
|
"github.com/go-redis/redis"
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RedisService struct{}
|
||||||
|
|
||||||
|
type IRedisService interface {
|
||||||
|
SearchWithPage(search dto.SearchRedisWithPage) (int64, interface{}, error)
|
||||||
|
Set(setData dto.RedisDataSet) error
|
||||||
|
Delete(info dto.RedisDelBatch) error
|
||||||
|
CleanAll(db int) error
|
||||||
|
|
||||||
|
// Backup(db dto.BackupDB) error
|
||||||
|
// Recover(db dto.RecoverDB) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIRedisService() IRedisService {
|
||||||
|
return &RedisService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *RedisService) SearchWithPage(search dto.SearchRedisWithPage) (int64, interface{}, error) {
|
||||||
|
redisInfo, err := mysqlRepo.LoadBaseInfoByKey("redis")
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("localhost:%v", redisInfo.Port),
|
||||||
|
Password: "eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81",
|
||||||
|
DB: search.DB,
|
||||||
|
})
|
||||||
|
total, err := client.DbSize().Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
keys, _, err := client.Scan(uint64((search.Page-1)*search.PageSize), "*", int64(search.PageSize)).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
var data []dto.RedisData
|
||||||
|
for _, key := range keys {
|
||||||
|
var dataItem dto.RedisData
|
||||||
|
dataItem.Key = key
|
||||||
|
value, err := client.Get(key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
dataItem.Value = value
|
||||||
|
typeVal, err := client.Type(key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
dataItem.Type = typeVal
|
||||||
|
length, err := client.StrLen(key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
dataItem.Length = length
|
||||||
|
ttl, err := client.TTL(key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
dataItem.Expiration = int64(ttl / 1000000000)
|
||||||
|
data = append(data, dataItem)
|
||||||
|
}
|
||||||
|
return total, data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *RedisService) Set(setData dto.RedisDataSet) error {
|
||||||
|
redisInfo, err := mysqlRepo.LoadBaseInfoByKey("redis")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("localhost:%v", redisInfo.Port),
|
||||||
|
Password: "eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81",
|
||||||
|
DB: setData.DB,
|
||||||
|
})
|
||||||
|
value, _ := client.Get(setData.Key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(value) != 0 {
|
||||||
|
if _, err := client.Del(setData.Key).Result(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := client.Set(setData.Key, setData.Value, time.Duration(setData.Expiration*int64(time.Second))).Result(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *RedisService) Delete(req dto.RedisDelBatch) error {
|
||||||
|
redisInfo, err := mysqlRepo.LoadBaseInfoByKey("redis")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("localhost:%v", redisInfo.Port),
|
||||||
|
Password: "eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81",
|
||||||
|
DB: req.DB,
|
||||||
|
})
|
||||||
|
if _, err := client.Del(req.Names...).Result(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *RedisService) CleanAll(db int) error {
|
||||||
|
redisInfo, err := mysqlRepo.LoadBaseInfoByKey("redis")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("localhost:%v", redisInfo.Port),
|
||||||
|
Password: "eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81",
|
||||||
|
DB: db,
|
||||||
|
})
|
||||||
|
if _, err := client.FlushAll().Result(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,30 +1,62 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||||
|
"github.com/go-redis/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMysql(t *testing.T) {
|
func TestMysql(t *testing.T) {
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: "localhost:6379",
|
||||||
|
Password: "eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81",
|
||||||
|
DB: 0,
|
||||||
|
})
|
||||||
|
// fmt.Println(rdb.Get("dqwas"))
|
||||||
|
|
||||||
gzipFile, err := os.Open("/tmp/ko.sql.gz")
|
client.Set("omg", "111", 10*time.Minute)
|
||||||
|
client.Set("omg1", "111", 10*time.Minute)
|
||||||
|
client.Set("omg2", "111", 10*time.Minute)
|
||||||
|
client.Set("omg3", "111", 10*time.Minute)
|
||||||
|
client.Set("omg4", "111", 10*time.Minute)
|
||||||
|
client.Set("omg5", "111", 10*time.Minute)
|
||||||
|
client.Set("omg6", "111", 10*time.Minute)
|
||||||
|
client.Set("omg7", "111", 10*time.Minute)
|
||||||
|
client.Set("omg8", "111", 10*time.Minute)
|
||||||
|
client.Set("omg9", "111", 10*time.Minute)
|
||||||
|
keys, _, err := client.Scan(0, "*", 5).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer gzipFile.Close()
|
|
||||||
gzipReader, err := gzip.NewReader(gzipFile)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
defer gzipReader.Close()
|
|
||||||
|
|
||||||
cmd := exec.Command("docker", "exec", "-i", "365", "mysql", "-uroot", "-pCalong@2012", "kubeoperator")
|
var data []dto.RedisData
|
||||||
cmd.Stdin = gzipReader
|
for _, key := range keys {
|
||||||
stdout, err := cmd.CombinedOutput()
|
var dataItem dto.RedisData
|
||||||
fmt.Println(string(stdout), err)
|
dataItem.Key = key
|
||||||
|
value, err := client.Get(key).Result()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
dataItem.Value = value
|
||||||
|
typeVal, err := client.Type(key).Result()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
dataItem.Type = typeVal
|
||||||
|
length, err := client.StrLen(key).Result()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
dataItem.Length = length
|
||||||
|
ttl, err := client.TTL(key).Result()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
dataItem.Expiration = int64(ttl / 1000000000)
|
||||||
|
data = append(data, dataItem)
|
||||||
|
}
|
||||||
|
fmt.Println(data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ type ServiceGroup struct {
|
||||||
ComposeTemplateService
|
ComposeTemplateService
|
||||||
|
|
||||||
MysqlService
|
MysqlService
|
||||||
|
RedisService
|
||||||
|
|
||||||
CronjobService
|
CronjobService
|
||||||
|
|
||||||
|
|
|
@ -34,5 +34,10 @@ func (s *DatabaseRouter) InitDatabaseRouter(Router *gin.RouterGroup) {
|
||||||
cmdRouter.GET("/baseinfo/:version", baseApi.LoadBaseinfo)
|
cmdRouter.GET("/baseinfo/:version", baseApi.LoadBaseinfo)
|
||||||
cmdRouter.GET("/versions", baseApi.LoadVersions)
|
cmdRouter.GET("/versions", baseApi.LoadVersions)
|
||||||
cmdRouter.GET("/dbs/:version", baseApi.ListDBNameByVersion)
|
cmdRouter.GET("/dbs/:version", baseApi.ListDBNameByVersion)
|
||||||
|
|
||||||
|
cmdRouter.POST("/redis/search", baseApi.SearchRedis)
|
||||||
|
withRecordRouter.POST("/redis", baseApi.SetRedis)
|
||||||
|
withRecordRouter.POST("/redis/del", baseApi.DeleteRedis)
|
||||||
|
withRecordRouter.POST("/redis/clean/:db", baseApi.CleanRedis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ export namespace Backup {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
source: string;
|
source: string;
|
||||||
|
backupType: string;
|
||||||
fileDir: string;
|
fileDir: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,4 +102,27 @@ export namespace Database {
|
||||||
operation: string;
|
operation: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redis
|
||||||
|
export interface SearchRedisWithPage extends ReqPage {
|
||||||
|
db: number;
|
||||||
|
}
|
||||||
|
export interface RedisData {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
length: number;
|
||||||
|
expiration: number;
|
||||||
|
}
|
||||||
|
export interface RedisDataSet {
|
||||||
|
db: number;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
expiration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RedisDelBatch {
|
||||||
|
db: number;
|
||||||
|
names: Array<string>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ import { Database } from '../interface/database';
|
||||||
export const searchMysqlDBs = (params: Database.Search) => {
|
export const searchMysqlDBs = (params: Database.Search) => {
|
||||||
return http.post<ResPage<Database.MysqlDBInfo>>(`databases/search`, params);
|
return http.post<ResPage<Database.MysqlDBInfo>>(`databases/search`, params);
|
||||||
};
|
};
|
||||||
|
export const searchRedisDBs = (params: Database.SearchRedisWithPage) => {
|
||||||
|
return http.post<ResPage<Database.RedisData>>(`databases/redis/search`, params);
|
||||||
|
};
|
||||||
export const listDBByVersion = (params: string) => {
|
export const listDBByVersion = (params: string) => {
|
||||||
return http.get(`databases/dbs/${params}`);
|
return http.get(`databases/dbs/${params}`);
|
||||||
};
|
};
|
||||||
|
@ -45,3 +48,14 @@ export const loadMysqlStatus = (param: string) => {
|
||||||
export const loadVersions = () => {
|
export const loadVersions = () => {
|
||||||
return http.get(`/databases/versions`);
|
return http.get(`/databases/versions`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// redis
|
||||||
|
export const setRedis = (params: Database.RedisDataSet) => {
|
||||||
|
return http.post(`/databases/redis`, params);
|
||||||
|
};
|
||||||
|
export const deleteRedisKey = (params: Database.RedisDelBatch) => {
|
||||||
|
return http.post(`/databases/redis/del`, params);
|
||||||
|
};
|
||||||
|
export const cleanRedisKey = (db: number) => {
|
||||||
|
return http.post(`/databases/redis/clean/${db}`);
|
||||||
|
};
|
||||||
|
|
|
@ -218,6 +218,16 @@ export default {
|
||||||
tableOpenCacheHelper: '表缓存',
|
tableOpenCacheHelper: '表缓存',
|
||||||
maxConnectionsHelper: '最大连接数',
|
maxConnectionsHelper: '最大连接数',
|
||||||
restart: '重启数据库',
|
restart: '重启数据库',
|
||||||
|
|
||||||
|
key: '键',
|
||||||
|
value: '值',
|
||||||
|
type: '数据类型',
|
||||||
|
length: '数据长度',
|
||||||
|
expiration: '有效期',
|
||||||
|
cleanAll: '清除所有',
|
||||||
|
expirationHelper: '有效期为 0 表示永久',
|
||||||
|
forever: '永久',
|
||||||
|
second: '秒',
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
operatorHelper: '将对选中容器进行 {0} 操作,是否继续?',
|
operatorHelper: '将对选中容器进行 {0} 操作,是否继续?',
|
||||||
|
|
|
@ -20,7 +20,7 @@ const databaseRouter = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/redis',
|
path: 'redis',
|
||||||
name: 'Redis',
|
name: 'Redis',
|
||||||
component: () => import('@/views/database/redis/index.vue'),
|
component: () => import('@/views/database/redis/index.vue'),
|
||||||
hidden: true,
|
hidden: true,
|
||||||
|
|
|
@ -11,13 +11,13 @@
|
||||||
<el-button type="primary" @click="onBackup()">
|
<el-button type="primary" @click="onBackup()">
|
||||||
{{ $t('database.backup') }}
|
{{ $t('database.backup') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete">
|
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
|
||||||
{{ $t('commons.button.delete') }}
|
{{ $t('commons.button.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
<el-table-column type="selection" fix />
|
<el-table-column type="selection" fix />
|
||||||
<el-table-column :label="$t('commons.table.name')" prop="fileName" show-overflow-tooltip />
|
<el-table-column :label="$t('commons.table.name')" prop="fileName" show-overflow-tooltip />
|
||||||
<el-table-column :label="$t('database.source')" prop="source" />
|
<el-table-column :label="$t('database.source')" prop="backupType" />
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="createdAt"
|
prop="createdAt"
|
||||||
:label="$t('commons.table.date')"
|
:label="$t('commons.table.date')"
|
||||||
|
|
|
@ -1,49 +1,107 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Submenu activeName="redis" />
|
<Submenu activeName="redis" />
|
||||||
<ComplexTable
|
<el-card style="margin-top: 20px">
|
||||||
:pagination-config="paginationConfig"
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" @search="search" :data="data">
|
||||||
v-model:selects="selects"
|
<template #toolbar>
|
||||||
@search="search"
|
<el-button type="primary" @click="onOperate">{{ $t('commons.button.create') }}</el-button>
|
||||||
style="margin-top: 20px"
|
<el-button type="primary" @click="onCleanAll">{{ $t('database.cleanAll') }}</el-button>
|
||||||
:data="data"
|
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
|
||||||
>
|
{{ $t('commons.button.delete') }}
|
||||||
<template #toolbar>
|
</el-button>
|
||||||
<el-button type="primary" @click="onOpenDialog()">{{ $t('commons.button.create') }}</el-button>
|
<el-select v-model="currentDB" @change="search" style="margin-left: 20px">
|
||||||
<el-button type="danger" plain :disabled="selects.length === 0" @click="onBatchDelete(null)">
|
<el-option
|
||||||
{{ $t('commons.button.delete') }}
|
v-for="item in dbOptions"
|
||||||
</el-button>
|
:key="item.label"
|
||||||
</template>
|
:value="item.value"
|
||||||
<el-table-column type="selection" fix />
|
:label="item.label"
|
||||||
<el-table-column :label="$t('commons.table.name')" prop="name" />
|
/>
|
||||||
<el-table-column :label="$t('auth.username')" prop="username" />
|
</el-select>
|
||||||
<el-table-column :label="$t('auth.password')" prop="password" />
|
</template>
|
||||||
<el-table-column :label="$t('commons.table.description')" prop="description" />
|
<el-table-column type="selection" fix />
|
||||||
<el-table-column
|
<el-table-column :label="$t('database.key')" prop="key" />
|
||||||
prop="createdAt"
|
<el-table-column :label="$t('database.value')" prop="value" />
|
||||||
:label="$t('commons.table.date')"
|
<el-table-column :label="$t('database.type')" prop="type" />
|
||||||
:formatter="dateFromat"
|
<el-table-column :label="$t('database.length')" prop="length" />
|
||||||
show-overflow-tooltip
|
<el-table-column :label="$t('database.expiration')" prop="expiration">
|
||||||
/>
|
<template #default="{ row }">
|
||||||
<fu-table-operations type="icon" :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
<span v-if="row.expiration === -1">{{ $t('database.forever') }}</span>
|
||||||
</ComplexTable>
|
<span v-else>{{ row.expiration }} {{ $t('database.second') }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||||
|
</ComplexTable>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<OperatrDialog @search="search" ref="dialogRef" />
|
<el-dialog v-model="redisVisiable" :destroy-on-close="true" width="30%">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>{{ $t('database.changePassword') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item prop="db">
|
||||||
|
<el-select v-model="form.db">
|
||||||
|
<el-option
|
||||||
|
v-for="item in dbOptions"
|
||||||
|
:key="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('database.key')" prop="key">
|
||||||
|
<el-input clearable v-model="form.key"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('database.value')" prop="value">
|
||||||
|
<el-input clearable v-model="form.value"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('database.expiration')" prop="expiration">
|
||||||
|
<el-input type="number" clearable v-model.number="form.expiration">
|
||||||
|
<template #append>{{ $t('database.second') }}</template>
|
||||||
|
</el-input>
|
||||||
|
<span class="input-help">{{ $t('database.expirationHelper') }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="redisVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
<el-button @click="submit(formRef)">
|
||||||
|
{{ $t('commons.button.confirm') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
import OperatrDialog from '@/views/database/create/index.vue';
|
|
||||||
import Submenu from '@/views/database/index.vue';
|
import Submenu from '@/views/database/index.vue';
|
||||||
import { dateFromat } from '@/utils/util';
|
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
import { deleteMysqlDB, searchMysqlDBs } from '@/api/modules/database';
|
import { cleanRedisKey, deleteRedisKey, searchRedisDBs, setRedis } from '@/api/modules/database';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import { Cronjob } from '@/api/interface/cronjob';
|
|
||||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||||
|
import { Database } from '@/api/interface/database';
|
||||||
|
import { ElForm, ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
|
||||||
const selects = ref<any>([]);
|
const selects = ref<any>([]);
|
||||||
|
const currentDB = ref(0);
|
||||||
|
const dbOptions = ref([
|
||||||
|
{ label: 'DB0', value: 0 },
|
||||||
|
{ label: 'DB1', value: 1 },
|
||||||
|
{ label: 'DB2', value: 2 },
|
||||||
|
{ label: 'DB3', value: 3 },
|
||||||
|
{ label: 'DB4', value: 4 },
|
||||||
|
{ label: 'DB5', value: 5 },
|
||||||
|
{ label: 'DB6', value: 6 },
|
||||||
|
{ label: 'DB7', value: 7 },
|
||||||
|
{ label: 'DB8', value: 8 },
|
||||||
|
{ label: 'DB9', value: 9 },
|
||||||
|
]);
|
||||||
|
|
||||||
const data = ref();
|
const data = ref();
|
||||||
const paginationConfig = reactive({
|
const paginationConfig = reactive({
|
||||||
|
@ -52,39 +110,100 @@ const paginationConfig = reactive({
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dialogRef = ref();
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
const onOpenDialog = async () => {
|
const formRef = ref<FormInstance>();
|
||||||
dialogRef.value!.acceptParams();
|
const form = reactive({
|
||||||
};
|
key: '',
|
||||||
|
value: '',
|
||||||
|
db: 0,
|
||||||
|
expiration: 0,
|
||||||
|
});
|
||||||
|
const rules = reactive({
|
||||||
|
db: [Rules.requiredSelect],
|
||||||
|
key: [Rules.requiredInput],
|
||||||
|
value: [Rules.requiredInput],
|
||||||
|
expiration: [Rules.requiredInput, Rules.number],
|
||||||
|
});
|
||||||
|
const redisVisiable = ref(false);
|
||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
let params = {
|
let params = {
|
||||||
page: paginationConfig.currentPage,
|
page: paginationConfig.currentPage,
|
||||||
pageSize: paginationConfig.pageSize,
|
pageSize: paginationConfig.pageSize,
|
||||||
|
db: currentDB.value,
|
||||||
};
|
};
|
||||||
const res = await searchMysqlDBs(params);
|
const res = await searchRedisDBs(params);
|
||||||
data.value = res.data.items || [];
|
data.value = res.data.items || [];
|
||||||
paginationConfig.total = res.data.total;
|
paginationConfig.total = res.data.total;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBatchDelete = async (row: Cronjob.CronjobInfo | null) => {
|
const onBatchDelete = async (row: Database.RedisData | null) => {
|
||||||
let ids: Array<number> = [];
|
let names: Array<string> = [];
|
||||||
if (row) {
|
if (row) {
|
||||||
ids.push(row.id);
|
names.push(row.key);
|
||||||
} else {
|
} else {
|
||||||
selects.value.forEach((item: Cronjob.CronjobInfo) => {
|
selects.value.forEach((item: Database.RedisData) => {
|
||||||
ids.push(item.id);
|
names.push(item.key);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await useDeleteData(deleteMysqlDB, { ids: ids }, 'commons.msg.delete', true);
|
let params = {
|
||||||
|
db: form.db,
|
||||||
|
names: names,
|
||||||
|
};
|
||||||
|
await useDeleteData(deleteRedisKey, params, 'commons.msg.delete', true);
|
||||||
search();
|
search();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCleanAll = async () => {
|
||||||
|
ElMessageBox.confirm(i18n.global.t('commons.msg.delete') + '?', i18n.global.t('database.cleanAll'), {
|
||||||
|
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||||
|
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||||
|
type: 'warning',
|
||||||
|
draggable: true,
|
||||||
|
}).then(async () => {
|
||||||
|
await cleanRedisKey(currentDB.value);
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOperate = async (row: Database.RedisData | undefined) => {
|
||||||
|
if (row) {
|
||||||
|
form.db = currentDB.value;
|
||||||
|
form.key = row.key;
|
||||||
|
form.value = row.value;
|
||||||
|
form.expiration = row.expiration === -1 ? 0 : row.expiration;
|
||||||
|
} else {
|
||||||
|
form.db = currentDB.value;
|
||||||
|
form.key = '';
|
||||||
|
form.value = '';
|
||||||
|
form.expiration = 0;
|
||||||
|
}
|
||||||
|
redisVisiable.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
await setRedis(form);
|
||||||
|
redisVisiable.value = false;
|
||||||
|
currentDB.value = form.db;
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
|
{
|
||||||
|
label: i18n.global.t('commons.button.edit'),
|
||||||
|
click: (row: Database.RedisData) => {
|
||||||
|
onOperate(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: i18n.global.t('commons.button.delete'),
|
label: i18n.global.t('commons.button.delete'),
|
||||||
icon: 'Delete',
|
click: (row: Database.RedisData) => {
|
||||||
click: (row: Cronjob.CronjobInfo) => {
|
|
||||||
onBatchDelete(row);
|
onBatchDelete(row);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -16,6 +16,7 @@ require (
|
||||||
github.com/gin-gonic/gin v1.8.1
|
github.com/gin-gonic/gin v1.8.1
|
||||||
github.com/go-gormigrate/gormigrate/v2 v2.0.2
|
github.com/go-gormigrate/gormigrate/v2 v2.0.2
|
||||||
github.com/go-playground/validator/v10 v10.11.0
|
github.com/go-playground/validator/v10 v10.11.0
|
||||||
|
github.com/go-redis/redis v6.15.9+incompatible
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/gogf/gf v1.16.9
|
github.com/gogf/gf v1.16.9
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.2
|
github.com/golang-jwt/jwt/v4 v4.4.2
|
||||||
|
@ -118,6 +119,8 @@ require (
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
|
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
|
||||||
|
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||||
|
github.com/onsi/gomega v1.21.1 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/runc v1.1.4 // indirect
|
github.com/opencontainers/runc v1.1.4 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
|
|
14
go.sum
14
go.sum
|
@ -401,9 +401,12 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn
|
||||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||||
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||||
|
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||||
|
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
||||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||||
|
@ -715,6 +718,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
|
||||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
|
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
|
||||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
|
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
|
@ -728,13 +733,18 @@ github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+
|
||||||
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
||||||
|
github.com/onsi/gomega v1.21.1 h1:OB/euWYIExnPBohllTicTHmGTrMaqJ67nIu80j0/uEM=
|
||||||
|
github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
|
||||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
|
@ -1083,6 +1093,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
@ -1196,6 +1207,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -1298,6 +1310,7 @@ golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4X
|
||||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
@ -1440,6 +1453,7 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|
Loading…
Reference in New Issue