mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-12 06:24:10 +01:00
console: implement "filter/saved" endpoints
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"akvorado/common/reporter"
|
"akvorado/common/reporter"
|
||||||
"akvorado/console"
|
"akvorado/console"
|
||||||
"akvorado/console/authentication"
|
"akvorado/console/authentication"
|
||||||
|
"akvorado/console/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConsoleConfiguration represents the configuration file for the console command.
|
// ConsoleConfiguration represents the configuration file for the console command.
|
||||||
@@ -20,6 +21,7 @@ type ConsoleConfiguration struct {
|
|||||||
Console console.Configuration
|
Console console.Configuration
|
||||||
ClickHouse clickhousedb.Configuration
|
ClickHouse clickhousedb.Configuration
|
||||||
Auth authentication.Configuration
|
Auth authentication.Configuration
|
||||||
|
Database database.Configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConsoleConfiguration is the default configuration for the console command.
|
// DefaultConsoleConfiguration is the default configuration for the console command.
|
||||||
@@ -30,6 +32,7 @@ func DefaultConsoleConfiguration() ConsoleConfiguration {
|
|||||||
Console: console.DefaultConfiguration(),
|
Console: console.DefaultConfiguration(),
|
||||||
ClickHouse: clickhousedb.DefaultConfiguration(),
|
ClickHouse: clickhousedb.DefaultConfiguration(),
|
||||||
Auth: authentication.DefaultConfiguration(),
|
Auth: authentication.DefaultConfiguration(),
|
||||||
|
Database: database.DefaultConfiguration(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,10 +95,16 @@ func consoleStart(r *reporter.Reporter, config ConsoleConfiguration, checkOnly b
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to initialize authentication component: %w", err)
|
return fmt.Errorf("unable to initialize authentication component: %w", err)
|
||||||
}
|
}
|
||||||
|
databaseComponent, err := database.New(r, config.Database)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to initialize database component: %w", err)
|
||||||
|
}
|
||||||
consoleComponent, err := console.New(r, config.Console, console.Dependencies{
|
consoleComponent, err := console.New(r, config.Console, console.Dependencies{
|
||||||
Daemon: daemonComponent,
|
Daemon: daemonComponent,
|
||||||
HTTP: httpComponent,
|
HTTP: httpComponent,
|
||||||
ClickHouseDB: clickhouseComponent,
|
ClickHouseDB: clickhouseComponent,
|
||||||
|
Auth: authenticationComponent,
|
||||||
|
Database: databaseComponent,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to initialize console component: %w", err)
|
return fmt.Errorf("unable to initialize console component: %w", err)
|
||||||
@@ -115,6 +124,7 @@ func consoleStart(r *reporter.Reporter, config ConsoleConfiguration, checkOnly b
|
|||||||
httpComponent,
|
httpComponent,
|
||||||
clickhouseComponent,
|
clickhouseComponent,
|
||||||
authenticationComponent,
|
authenticationComponent,
|
||||||
|
databaseComponent,
|
||||||
consoleComponent,
|
consoleComponent,
|
||||||
}
|
}
|
||||||
return StartStopComponents(r, daemonComponent, components)
|
return StartStopComponents(r, daemonComponent, components)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ func Diff(a, b interface{}) string {
|
|||||||
// HTTPEndpointCases describes case for TestHTTPEndpoints
|
// HTTPEndpointCases describes case for TestHTTPEndpoints
|
||||||
type HTTPEndpointCases []struct {
|
type HTTPEndpointCases []struct {
|
||||||
Description string
|
Description string
|
||||||
|
Method string
|
||||||
URL string
|
URL string
|
||||||
Header http.Header
|
Header http.Header
|
||||||
JSONInput interface{}
|
JSONInput interface{}
|
||||||
@@ -64,8 +65,15 @@ func TestHTTPEndpoints(t *testing.T, serverAddr net.Addr, cases HTTPEndpointCase
|
|||||||
}
|
}
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
var err error
|
var err error
|
||||||
|
if tc.Method == "" {
|
||||||
|
if tc.JSONInput == nil {
|
||||||
|
tc.Method = "GET"
|
||||||
|
} else {
|
||||||
|
tc.Method = "POST"
|
||||||
|
}
|
||||||
|
}
|
||||||
if tc.JSONInput == nil {
|
if tc.JSONInput == nil {
|
||||||
req, _ := http.NewRequest("GET",
|
req, _ := http.NewRequest(tc.Method,
|
||||||
fmt.Sprintf("http://%s%s", serverAddr, tc.URL),
|
fmt.Sprintf("http://%s%s", serverAddr, tc.URL),
|
||||||
nil)
|
nil)
|
||||||
if tc.Header != nil {
|
if tc.Header != nil {
|
||||||
@@ -73,7 +81,7 @@ func TestHTTPEndpoints(t *testing.T, serverAddr net.Addr, cases HTTPEndpointCase
|
|||||||
}
|
}
|
||||||
resp, err = http.DefaultClient.Do(req)
|
resp, err = http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GET %s:\n%+v", tc.URL, err)
|
t.Fatalf("%s %s:\n%+v", tc.Method, tc.URL, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
payload := new(bytes.Buffer)
|
payload := new(bytes.Buffer)
|
||||||
@@ -81,7 +89,7 @@ func TestHTTPEndpoints(t *testing.T, serverAddr net.Addr, cases HTTPEndpointCase
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Encode() error:\n%+v", err)
|
t.Fatalf("Encode() error:\n%+v", err)
|
||||||
}
|
}
|
||||||
req, _ := http.NewRequest("POST",
|
req, _ := http.NewRequest(tc.Method,
|
||||||
fmt.Sprintf("http://%s%s", serverAddr, tc.URL),
|
fmt.Sprintf("http://%s%s", serverAddr, tc.URL),
|
||||||
payload)
|
payload)
|
||||||
if tc.Header != nil {
|
if tc.Header != nil {
|
||||||
@@ -90,7 +98,7 @@ func TestHTTPEndpoints(t *testing.T, serverAddr net.Addr, cases HTTPEndpointCase
|
|||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
resp, err = http.DefaultClient.Do(req)
|
resp, err = http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("POST %s:\n%+v", tc.URL, err)
|
t.Fatalf("%s %s:\n%+v", tc.Method, tc.URL, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ func TestServeAssets(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
conf := DefaultConfiguration()
|
conf := DefaultConfiguration()
|
||||||
conf.ServeLiveFS = live
|
conf.ServeLiveFS = live
|
||||||
c, h, _, _ := NewMock(t, conf)
|
_, h, _, _ := NewMock(t, conf)
|
||||||
helpers.StartStop(t, c)
|
|
||||||
|
|
||||||
helpers.TestHTTPEndpoints(t, h.Address, helpers.HTTPEndpointCases{
|
helpers.TestHTTPEndpoints(t, h.Address, helpers.HTTPEndpointCases{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -169,7 +169,6 @@ func TestQueryFlowsTables(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c, _, _, _ := NewMock(t, DefaultConfiguration())
|
c, _, _, _ := NewMock(t, DefaultConfiguration())
|
||||||
helpers.StartStop(t, c)
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.Description, func(t *testing.T) {
|
t.Run(tc.Description, func(t *testing.T) {
|
||||||
c.flowsTables = tc.Tables
|
c.flowsTables = tc.Tables
|
||||||
|
|||||||
@@ -331,9 +331,9 @@ resolutions:
|
|||||||
|
|
||||||
## Console service
|
## Console service
|
||||||
|
|
||||||
The main components of the console service are `http` and `console`.
|
The main components of the console service are `http`, `console`,
|
||||||
`http` accepts the [same configuration](#http) as for the inlet
|
`authentication` and `database`. `http` accepts the [same
|
||||||
service.
|
configuration](#http) as for the inlet service.
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
@@ -352,16 +352,15 @@ is also possible to modify the default user (when no header is
|
|||||||
present) by tweaking the `default-user` key:
|
present) by tweaking the `default-user` key:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
console:
|
authentication:
|
||||||
authentication:
|
headers:
|
||||||
headers:
|
login: Remote-User
|
||||||
login: Remote-User
|
name: Remote-Name
|
||||||
name: Remote-Name
|
email: Remote-Email
|
||||||
email: Remote-Email
|
logout-url: X-Logout-URL
|
||||||
logout-url: X-Logout-URL
|
default-user:
|
||||||
default-user:
|
login: default
|
||||||
login: default
|
name: Default User
|
||||||
name: Default User
|
|
||||||
```
|
```
|
||||||
|
|
||||||
To prevent access when not authenticated, the `login` field for the
|
To prevent access when not authenticated, the `login` field for the
|
||||||
@@ -382,3 +381,16 @@ There also exist simpler solutions only providing authentication:
|
|||||||
|
|
||||||
- [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy/), associated with [Dex](https://dexidp.io/)
|
- [OAuth2 Proxy](https://oauth2-proxy.github.io/oauth2-proxy/), associated with [Dex](https://dexidp.io/)
|
||||||
- [Ory](https://www.ory.sh), notably Hydra and Oathkeeper
|
- [Ory](https://www.ory.sh), notably Hydra and Oathkeeper
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
The console stores some data, like per-user filters, into a relational
|
||||||
|
database. When the database is not configured, data is only stored in
|
||||||
|
memory and will be lost on restart. Currently, the only accepted
|
||||||
|
driver is SQLite.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
database:
|
||||||
|
driver: sqlite
|
||||||
|
dsn: /var/lib/akvorado/console.sqlite
|
||||||
|
```
|
||||||
|
|||||||
17
console/database/config.go
Normal file
17
console/database/config.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
// Configuration describes the configuration for the authentication component.
|
||||||
|
type Configuration struct {
|
||||||
|
// Driver defines the driver for the database
|
||||||
|
Driver string
|
||||||
|
// DSN defines the DSN to connect to the database
|
||||||
|
DSN string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfiguration represents the default configuration for the console component.
|
||||||
|
func DefaultConfiguration() Configuration {
|
||||||
|
return Configuration{
|
||||||
|
Driver: "sqlite",
|
||||||
|
DSN: "file::memory:?cache=shared",
|
||||||
|
}
|
||||||
|
}
|
||||||
47
console/database/logs.go
Normal file
47
console/database/logs.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gormlogger "gorm.io/gorm/logger"
|
||||||
|
"gorm.io/gorm/utils"
|
||||||
|
|
||||||
|
"akvorado/common/reporter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logger struct {
|
||||||
|
r *reporter.Reporter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) LogMode(gormlogger.LogLevel) gormlogger.Interface {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Info(ctx context.Context, s string, args ...interface{}) {
|
||||||
|
l.r.Info().Msgf(s, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Warn(ctx context.Context, s string, args ...interface{}) {
|
||||||
|
l.r.Warn().Msgf(s, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Error(ctx context.Context, s string, args ...interface{}) {
|
||||||
|
l.r.Error().Msgf(s, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
||||||
|
elapsed := time.Since(begin)
|
||||||
|
sql, _ := fc()
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"sql": sql,
|
||||||
|
"duration": elapsed,
|
||||||
|
"source": utils.FileWithLineNum(),
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
l.r.Error().Err(err).Fields(fields).Msg("SQL query error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.r.Debug().Fields(fields).Msg("SQL query")
|
||||||
|
}
|
||||||
61
console/database/root.go
Normal file
61
console/database/root.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"akvorado/common/reporter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Component represents the database compomenent.
|
||||||
|
type Component struct {
|
||||||
|
r *reporter.Reporter
|
||||||
|
config Configuration
|
||||||
|
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new database component.
|
||||||
|
func New(r *reporter.Reporter, configuration Configuration) (*Component, error) {
|
||||||
|
c := Component{
|
||||||
|
r: r,
|
||||||
|
config: configuration,
|
||||||
|
}
|
||||||
|
switch c.config.Driver {
|
||||||
|
case "sqlite":
|
||||||
|
db, err := gorm.Open(sqlite.Open(c.config.DSN), &gorm.Config{
|
||||||
|
Logger: &logger{r},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to open database: %w", err)
|
||||||
|
}
|
||||||
|
c.db = db
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%q is not a supporter driver", c.config.Driver)
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the database component
|
||||||
|
func (c *Component) Start() error {
|
||||||
|
c.r.Info().Msg("starting database component")
|
||||||
|
if err := c.db.AutoMigrate(&SavedFilter{}); err != nil {
|
||||||
|
return fmt.Errorf("cannot migrate database: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the database component.
|
||||||
|
func (c *Component) Stop() error {
|
||||||
|
defer c.r.Info().Msg("database component stopped")
|
||||||
|
if c.db != nil {
|
||||||
|
sqlDB, err := c.db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sqlDB.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
47
console/database/saved_filters.go
Normal file
47
console/database/saved_filters.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SavedFilter represents a saved filter in database.
|
||||||
|
type SavedFilter struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
User string `gorm:"index" json:"user"`
|
||||||
|
Shared bool `json:"shared"`
|
||||||
|
Description string `json:"description" binding:"required"`
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSavedFilter creates a new saved filter in database.
|
||||||
|
func (c *Component) CreateSavedFilter(ctx context.Context, f SavedFilter) error {
|
||||||
|
result := c.db.WithContext(ctx).Omit("ID").Create(&f)
|
||||||
|
if result.Error != nil {
|
||||||
|
return fmt.Errorf("unable to create new saved filter: %w", result.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSavedFilters list all saved filters for the provided user
|
||||||
|
func (c *Component) ListSavedFilters(ctx context.Context, user string) ([]SavedFilter, error) {
|
||||||
|
var results []SavedFilter
|
||||||
|
result := c.db.WithContext(ctx).Where(&SavedFilter{User: user}).Find(&results)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, fmt.Errorf("unable to retrieve saved filters: %w", result.Error)
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSavedFilter deletes the provided saved filter
|
||||||
|
func (c *Component) DeleteSavedFilter(ctx context.Context, f SavedFilter) error {
|
||||||
|
result := c.db.WithContext(ctx).Where(&SavedFilter{User: f.User}).Delete(&f)
|
||||||
|
if result.Error != nil {
|
||||||
|
return fmt.Errorf("cannot delete saved filter: %w", result.Error)
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return errors.New("no matching saved filter to delete")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
84
console/database/saved_filters_test.go
Normal file
84
console/database/saved_filters_test.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"akvorado/common/helpers"
|
||||||
|
"akvorado/common/reporter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSavedFilter(t *testing.T) {
|
||||||
|
r := reporter.NewMock(t)
|
||||||
|
c := NewMock(t, r)
|
||||||
|
|
||||||
|
// Create
|
||||||
|
if err := c.CreateSavedFilter(context.Background(), SavedFilter{
|
||||||
|
ID: 17,
|
||||||
|
User: "marty",
|
||||||
|
Shared: false,
|
||||||
|
Description: "marty's filter",
|
||||||
|
Content: "SrcAS = 12322",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("CreateSavedFilter() error:\n%+v", err)
|
||||||
|
}
|
||||||
|
if err := c.CreateSavedFilter(context.Background(), SavedFilter{
|
||||||
|
User: "judith",
|
||||||
|
Shared: true,
|
||||||
|
Description: "judith's filter",
|
||||||
|
Content: "InIfBoundary = external",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("CreateSavedFilter() error:\n%+v", err)
|
||||||
|
}
|
||||||
|
if err := c.CreateSavedFilter(context.Background(), SavedFilter{
|
||||||
|
User: "marty",
|
||||||
|
Shared: true,
|
||||||
|
Description: "marty's second filter",
|
||||||
|
Content: "InIfBoundary = internal",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("CreateSavedFilter() error:\n%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
got, err := c.ListSavedFilters(context.Background(), "marty")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListSavedFilters() error:\n%+v", err)
|
||||||
|
}
|
||||||
|
if diff := helpers.Diff(got, []SavedFilter{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
User: "marty",
|
||||||
|
Shared: false,
|
||||||
|
Description: "marty's filter",
|
||||||
|
Content: "SrcAS = 12322",
|
||||||
|
}, {
|
||||||
|
ID: 3,
|
||||||
|
User: "marty",
|
||||||
|
Shared: true,
|
||||||
|
Description: "marty's second filter",
|
||||||
|
Content: "InIfBoundary = internal",
|
||||||
|
},
|
||||||
|
}); diff != "" {
|
||||||
|
t.Fatalf("ListSavedFilters() (-got, +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
if err := c.DeleteSavedFilter(context.Background(), SavedFilter{ID: 1}); err != nil {
|
||||||
|
t.Fatalf("DeleteSavedFilter() error:\n%+v", err)
|
||||||
|
}
|
||||||
|
got, _ = c.ListSavedFilters(context.Background(), "marty")
|
||||||
|
if diff := helpers.Diff(got, []SavedFilter{
|
||||||
|
{
|
||||||
|
ID: 3,
|
||||||
|
User: "marty",
|
||||||
|
Shared: true,
|
||||||
|
Description: "marty's second filter",
|
||||||
|
Content: "InIfBoundary = internal",
|
||||||
|
},
|
||||||
|
}); diff != "" {
|
||||||
|
t.Fatalf("ListSavedFilters() (-got, +want):\n%s", diff)
|
||||||
|
}
|
||||||
|
if err := c.DeleteSavedFilter(context.Background(), SavedFilter{ID: 1}); err == nil {
|
||||||
|
t.Fatal("DeleteSavedFilter() no error")
|
||||||
|
}
|
||||||
|
}
|
||||||
21
console/database/tests.go
Normal file
21
console/database/tests.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//go:build !release
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"akvorado/common/helpers"
|
||||||
|
"akvorado/common/reporter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewMock instantiantes a new authentication component
|
||||||
|
func NewMock(t *testing.T, r *reporter.Reporter) *Component {
|
||||||
|
t.Helper()
|
||||||
|
c, err := New(r, DefaultConfiguration())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New() error:\n%+v", err)
|
||||||
|
}
|
||||||
|
helpers.StartStop(t, c)
|
||||||
|
return c
|
||||||
|
}
|
||||||
@@ -6,8 +6,6 @@ import (
|
|||||||
netHTTP "net/http"
|
netHTTP "net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"akvorado/common/helpers"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServeDocs(t *testing.T) {
|
func TestServeDocs(t *testing.T) {
|
||||||
@@ -27,8 +25,7 @@ func TestServeDocs(t *testing.T) {
|
|||||||
t.Run(fmt.Sprintf("%s-%s", name, tc.Path), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s-%s", name, tc.Path), func(t *testing.T) {
|
||||||
conf := DefaultConfiguration()
|
conf := DefaultConfiguration()
|
||||||
conf.ServeLiveFS = live
|
conf.ServeLiveFS = live
|
||||||
c, h, _, _ := NewMock(t, conf)
|
_, h, _, _ := NewMock(t, conf)
|
||||||
helpers.StartStop(t, c)
|
|
||||||
|
|
||||||
resp, err := netHTTP.Get(fmt.Sprintf("http://%s/api/v0/console/docs/%s",
|
resp, err := netHTTP.Get(fmt.Sprintf("http://%s/api/v0/console/docs/%s",
|
||||||
h.Address, tc.Path))
|
h.Address, tc.Path))
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package console
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"akvorado/common/helpers"
|
"akvorado/common/helpers"
|
||||||
|
"akvorado/console/authentication"
|
||||||
|
"akvorado/console/database"
|
||||||
"akvorado/console/filter"
|
"akvorado/console/filter"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -268,3 +271,51 @@ LIMIT 20`, column, column, column, column, column)
|
|||||||
gc.JSON(http.StatusOK, filterCompleteHandlerOutput{filteredCompletions})
|
gc.JSON(http.StatusOK, filterCompleteHandlerOutput{filteredCompletions})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Component) filterSavedListHandlerFunc(gc *gin.Context) {
|
||||||
|
ctx := c.t.Context(gc.Request.Context())
|
||||||
|
user := gc.MustGet("user").(authentication.UserInformation).Login
|
||||||
|
filters, err := c.d.Database.ListSavedFilters(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
c.r.Err(err).Msg("unable to list filters")
|
||||||
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "unable to list filters"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gc.JSON(http.StatusOK, gin.H{"filters": filters})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) filterSavedDeleteHandlerFunc(gc *gin.Context) {
|
||||||
|
ctx := c.t.Context(gc.Request.Context())
|
||||||
|
user := gc.MustGet("user").(authentication.UserInformation).Login
|
||||||
|
id, err := strconv.ParseUint(gc.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
gc.JSON(http.StatusBadRequest, gin.H{"message": "bad ID format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := c.d.Database.DeleteSavedFilter(ctx, database.SavedFilter{
|
||||||
|
ID: uint(id),
|
||||||
|
User: user,
|
||||||
|
}); err != nil {
|
||||||
|
// Assume this is because it is not found
|
||||||
|
gc.JSON(http.StatusNotFound, gin.H{"message": "filter not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gc.JSON(http.StatusNoContent, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) filterSavedAddHandlerFunc(gc *gin.Context) {
|
||||||
|
ctx := c.t.Context(gc.Request.Context())
|
||||||
|
user := gc.MustGet("user").(authentication.UserInformation).Login
|
||||||
|
var filter database.SavedFilter
|
||||||
|
if err := gc.ShouldBindJSON(&filter); err != nil {
|
||||||
|
gc.JSON(http.StatusBadRequest, gin.H{"message": helpers.Capitalize(err.Error())})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filter.User = user
|
||||||
|
if err := c.d.Database.CreateSavedFilter(ctx, filter); err != nil {
|
||||||
|
c.r.Err(err).Msg("cannot create saved filter")
|
||||||
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "cannot create new filter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gc.JSON(http.StatusNoContent, nil)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package console
|
package console
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
netHTTP "net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -10,8 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFilterHandlers(t *testing.T) {
|
func TestFilterHandlers(t *testing.T) {
|
||||||
c, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
_, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
||||||
helpers.StartStop(t, c)
|
|
||||||
|
|
||||||
mockConn.EXPECT().
|
mockConn.EXPECT().
|
||||||
Select(gomock.Any(), gomock.Any(), `
|
Select(gomock.Any(), gomock.Any(), `
|
||||||
@@ -186,6 +186,69 @@ UNION DISTINCT
|
|||||||
{"label": "customer-2", "detail": "network name", "quoted": true},
|
{"label": "customer-2", "detail": "network name", "quoted": true},
|
||||||
{"label": "customer-3", "detail": "network name", "quoted": true},
|
{"label": "customer-3", "detail": "network name", "quoted": true},
|
||||||
}},
|
}},
|
||||||
|
}, {
|
||||||
|
Description: "list, no filters",
|
||||||
|
URL: "/api/v0/console/filter/saved",
|
||||||
|
StatusCode: 200,
|
||||||
|
JSONOutput: gin.H{"filters": []gin.H{}},
|
||||||
|
}, {
|
||||||
|
Description: "store one filter",
|
||||||
|
URL: "/api/v0/console/filter/saved",
|
||||||
|
StatusCode: 204,
|
||||||
|
JSONInput: gin.H{
|
||||||
|
"description": "test 1",
|
||||||
|
"content": "InIfBoundary = external",
|
||||||
|
},
|
||||||
|
ContentType: "application/json; charset=utf-8",
|
||||||
|
}, {
|
||||||
|
Description: "list stored filters",
|
||||||
|
URL: "/api/v0/console/filter/saved",
|
||||||
|
JSONOutput: gin.H{"filters": []gin.H{
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"shared": false,
|
||||||
|
"user": "__default",
|
||||||
|
"description": "test 1",
|
||||||
|
"content": "InIfBoundary = external",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}, {
|
||||||
|
Description: "list stored filters as another user",
|
||||||
|
URL: "/api/v0/console/filter/saved",
|
||||||
|
Header: func() netHTTP.Header {
|
||||||
|
headers := make(netHTTP.Header)
|
||||||
|
headers.Add("Remote-User", "alfred")
|
||||||
|
return headers
|
||||||
|
}(),
|
||||||
|
JSONOutput: gin.H{"filters": []gin.H{}},
|
||||||
|
}, {
|
||||||
|
Description: "delete stored filter as another user",
|
||||||
|
Method: "DELETE",
|
||||||
|
URL: "/api/v0/console/filter/saved/1",
|
||||||
|
Header: func() netHTTP.Header {
|
||||||
|
headers := make(netHTTP.Header)
|
||||||
|
headers.Add("Remote-User", "alfred")
|
||||||
|
return headers
|
||||||
|
}(),
|
||||||
|
StatusCode: 404,
|
||||||
|
JSONOutput: gin.H{"message": "filter not found"},
|
||||||
|
}, {
|
||||||
|
Description: "delete stored filter",
|
||||||
|
Method: "DELETE",
|
||||||
|
URL: "/api/v0/console/filter/saved/1",
|
||||||
|
StatusCode: 204,
|
||||||
|
ContentType: "application/json; charset=utf-8",
|
||||||
|
}, {
|
||||||
|
Description: "delete stored filter with invalid ID",
|
||||||
|
Method: "DELETE",
|
||||||
|
URL: "/api/v0/console/filter/saved/kjgdfhgh",
|
||||||
|
StatusCode: 400,
|
||||||
|
JSONOutput: gin.H{"message": "bad ID format"},
|
||||||
|
}, {
|
||||||
|
Description: "list stored filter after delete",
|
||||||
|
URL: "/api/v0/console/filter/saved",
|
||||||
|
StatusCode: 200,
|
||||||
|
JSONOutput: gin.H{"filters": []gin.H{}},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,8 +140,7 @@ ORDER BY time`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGraphHandler(t *testing.T) {
|
func TestGraphHandler(t *testing.T) {
|
||||||
c, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
_, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
||||||
helpers.StartStop(t, c)
|
|
||||||
|
|
||||||
base := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
base := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
||||||
expectedSQL := []struct {
|
expectedSQL := []struct {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"akvorado/common/http"
|
"akvorado/common/http"
|
||||||
"akvorado/common/reporter"
|
"akvorado/common/reporter"
|
||||||
"akvorado/console/authentication"
|
"akvorado/console/authentication"
|
||||||
|
"akvorado/console/database"
|
||||||
|
|
||||||
"github.com/benbjohnson/clock"
|
"github.com/benbjohnson/clock"
|
||||||
"gopkg.in/tomb.v2"
|
"gopkg.in/tomb.v2"
|
||||||
@@ -47,6 +48,7 @@ type Dependencies struct {
|
|||||||
ClickHouseDB *clickhousedb.Component
|
ClickHouseDB *clickhousedb.Component
|
||||||
Clock clock.Clock
|
Clock clock.Clock
|
||||||
Auth *authentication.Component
|
Auth *authentication.Component
|
||||||
|
Database *database.Component
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new console component.
|
// New creates a new console component.
|
||||||
@@ -88,6 +90,9 @@ func (c *Component) Start() error {
|
|||||||
endpoint.POST("/sankey", c.sankeyHandlerFunc)
|
endpoint.POST("/sankey", c.sankeyHandlerFunc)
|
||||||
endpoint.POST("/filter/validate", c.filterValidateHandlerFunc)
|
endpoint.POST("/filter/validate", c.filterValidateHandlerFunc)
|
||||||
endpoint.POST("/filter/complete", c.filterCompleteHandlerFunc)
|
endpoint.POST("/filter/complete", c.filterCompleteHandlerFunc)
|
||||||
|
endpoint.GET("/filter/saved", c.filterSavedListHandlerFunc)
|
||||||
|
endpoint.DELETE("/filter/saved/:id", c.filterSavedDeleteHandlerFunc)
|
||||||
|
endpoint.POST("/filter/saved", c.filterSavedAddHandlerFunc)
|
||||||
endpoint.GET("/user/info", c.d.Auth.UserInfoHandlerFunc)
|
endpoint.GET("/user/info", c.d.Auth.UserInfoHandlerFunc)
|
||||||
endpoint.GET("/user/avatar", c.d.Auth.UserAvatarHandlerFunc)
|
endpoint.GET("/user/avatar", c.d.Auth.UserAvatarHandlerFunc)
|
||||||
|
|
||||||
|
|||||||
@@ -118,8 +118,7 @@ ORDER BY xps DESC`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSankeyHandler(t *testing.T) {
|
func TestSankeyHandler(t *testing.T) {
|
||||||
c, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
_, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
||||||
helpers.StartStop(t, c)
|
|
||||||
|
|
||||||
expectedSQL := []struct {
|
expectedSQL := []struct {
|
||||||
Xps float64 `ch:"xps"`
|
Xps float64 `ch:"xps"`
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import (
|
|||||||
"akvorado/common/clickhousedb"
|
"akvorado/common/clickhousedb"
|
||||||
"akvorado/common/clickhousedb/mocks"
|
"akvorado/common/clickhousedb/mocks"
|
||||||
"akvorado/common/daemon"
|
"akvorado/common/daemon"
|
||||||
|
"akvorado/common/helpers"
|
||||||
"akvorado/common/http"
|
"akvorado/common/http"
|
||||||
"akvorado/common/reporter"
|
"akvorado/common/reporter"
|
||||||
"akvorado/console/authentication"
|
"akvorado/console/authentication"
|
||||||
|
"akvorado/console/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewMock instantiantes a new authentication component
|
// NewMock instantiantes a new authentication component
|
||||||
@@ -28,9 +30,11 @@ func NewMock(t *testing.T, config Configuration) (*Component, *http.Component, *
|
|||||||
ClickHouseDB: ch,
|
ClickHouseDB: ch,
|
||||||
Clock: mockClock,
|
Clock: mockClock,
|
||||||
Auth: authentication.NewMock(t, r),
|
Auth: authentication.NewMock(t, r),
|
||||||
|
Database: database.NewMock(t, r),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New() error:\n%+v", err)
|
t.Fatalf("New() error:\n%+v", err)
|
||||||
}
|
}
|
||||||
|
helpers.StartStop(t, c)
|
||||||
return c, h, mockConn, mockClock
|
return c, h, mockConn, mockClock
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestWidgetLastFlow(t *testing.T) {
|
func TestWidgetLastFlow(t *testing.T) {
|
||||||
c, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
_, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
||||||
helpers.StartStop(t, c)
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
mockRows := mocks.NewMockRows(ctrl)
|
mockRows := mocks.NewMockRows(ctrl)
|
||||||
@@ -91,8 +90,7 @@ func TestWidgetLastFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFlowRate(t *testing.T) {
|
func TestFlowRate(t *testing.T) {
|
||||||
c, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
_, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
||||||
helpers.StartStop(t, c)
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
mockRow := mocks.NewMockRow(ctrl)
|
mockRow := mocks.NewMockRow(ctrl)
|
||||||
@@ -115,8 +113,7 @@ func TestFlowRate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWidgetExporters(t *testing.T) {
|
func TestWidgetExporters(t *testing.T) {
|
||||||
c, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
_, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
||||||
helpers.StartStop(t, c)
|
|
||||||
|
|
||||||
expected := []struct {
|
expected := []struct {
|
||||||
ExporterName string
|
ExporterName string
|
||||||
@@ -146,8 +143,7 @@ func TestWidgetExporters(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWidgetTop(t *testing.T) {
|
func TestWidgetTop(t *testing.T) {
|
||||||
c, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
_, h, mockConn, _ := NewMock(t, DefaultConfiguration())
|
||||||
helpers.StartStop(t, c)
|
|
||||||
|
|
||||||
gomock.InOrder(
|
gomock.InOrder(
|
||||||
mockConn.EXPECT().
|
mockConn.EXPECT().
|
||||||
@@ -214,8 +210,7 @@ func TestWidgetTop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWidgetGraph(t *testing.T) {
|
func TestWidgetGraph(t *testing.T) {
|
||||||
c, h, mockConn, mockClock := NewMock(t, DefaultConfiguration())
|
_, h, mockConn, mockClock := NewMock(t, DefaultConfiguration())
|
||||||
helpers.StartStop(t, c)
|
|
||||||
|
|
||||||
base := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
base := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
||||||
mockClock.Set(base.Add(24 * time.Hour))
|
mockClock.Set(base.Add(24 * time.Hour))
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ services:
|
|||||||
|
|
||||||
clickhouse:
|
clickhouse:
|
||||||
image: clickhouse/clickhouse-server:22.2
|
image: clickhouse/clickhouse-server:22.2
|
||||||
depends_on:
|
|
||||||
- kafka
|
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8123:8123/tcp
|
- 127.0.0.1:8123:8123/tcp
|
||||||
- 127.0.0.1:9000:9000/tcp
|
- 127.0.0.1:9000:9000/tcp
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -31,6 +31,8 @@ require (
|
|||||||
google.golang.org/protobuf v1.27.1
|
google.golang.org/protobuf v1.27.1
|
||||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
gorm.io/driver/sqlite v1.3.4
|
||||||
|
gorm.io/gorm v1.23.5
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -59,9 +61,12 @@ require (
|
|||||||
github.com/jcmturner/gofork v1.0.0 // indirect
|
github.com/jcmturner/gofork v1.0.0 // indirect
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect
|
github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.15.1 // indirect
|
github.com/klauspost/compress v1.15.1 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.12 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||||
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
|
||||||
|
|||||||
12
go.sum
12
go.sum
@@ -327,6 +327,11 @@ github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJz
|
|||||||
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
|
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
@@ -382,6 +387,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
|||||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
@@ -1008,6 +1015,11 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/sqlite v1.3.4 h1:NnFOPVfzi4CPsJPH4wXr6rMkPb4ElHEqKMvrsx9c9Fk=
|
||||||
|
gorm.io/driver/sqlite v1.3.4/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U=
|
||||||
|
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
|
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
|
||||||
|
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
Reference in New Issue
Block a user