Browse Source

Handle decisions with varying expiry for same IP (#1262)

* Upgrade ent and add sql/modifier in codegen

* update db wrappers to sanitize LAPI

Signed-off-by: Shivam Sandbhor <shivam.sandbhor@gmail.com>
Shivam Sandbhor 3 năm trước cách đây
mục cha
commit
e4f6cdfc14

+ 26 - 17
go.mod

@@ -3,7 +3,7 @@ module github.com/crowdsecurity/crowdsec
 go 1.17
 
 require (
-	entgo.io/ent v0.9.1
+	entgo.io/ent v0.10.0
 	github.com/AlecAivazis/survey/v2 v2.2.7
 	github.com/Masterminds/sprig v2.22.0+incompatible
 	github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26
@@ -21,23 +21,23 @@ require (
 	github.com/docker/go-connections v0.4.0
 	github.com/enescakir/emoji v1.0.0
 	github.com/fatih/color v1.13.0
-	github.com/fsnotify/fsnotify v1.4.9
+	github.com/fsnotify/fsnotify v1.5.1
 	github.com/gin-gonic/gin v1.7.7
 	github.com/go-co-op/gocron v1.9.0
 	github.com/go-openapi/errors v0.20.1
 	github.com/go-openapi/strfmt v0.19.11
 	github.com/go-openapi/swag v0.19.12
 	github.com/go-openapi/validate v0.20.0
-	github.com/go-sql-driver/mysql v1.5.1-0.20200311113236-681ffa848bae
+	github.com/go-sql-driver/mysql v1.6.0
 	github.com/google/go-querystring v1.0.0
 	github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e
-	github.com/hashicorp/go-hclog v0.14.1
+	github.com/hashicorp/go-hclog v1.0.0
 	github.com/hashicorp/go-plugin v1.4.2
 	github.com/hashicorp/go-version v1.2.1
 	github.com/influxdata/go-syslog/v3 v3.0.0
 	github.com/jszwec/csvutil v1.5.1
-	github.com/lib/pq v1.10.2
-	github.com/mattn/go-sqlite3 v1.14.8
+	github.com/lib/pq v1.10.4
+	github.com/mattn/go-sqlite3 v1.14.10
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
 	github.com/nxadm/tail v1.4.6
 	github.com/olekukonko/tablewriter v0.0.5
@@ -49,11 +49,11 @@ require (
 	github.com/prometheus/prom2json v1.3.0
 	github.com/r3labs/diff/v2 v2.14.1
 	github.com/sirupsen/logrus v1.8.1
-	github.com/spf13/cobra v1.1.3
-	github.com/stretchr/testify v1.7.0
+	github.com/spf13/cobra v1.3.0
+	github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
 	golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce
-	golang.org/x/mod v0.4.2
-	google.golang.org/grpc v1.35.0
+	golang.org/x/mod v0.5.1
+	google.golang.org/grpc v1.42.0
 	google.golang.org/protobuf v1.27.1
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 	gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
@@ -62,22 +62,26 @@ require (
 )
 
 require (
+	ariga.io/atlas v0.3.2-0.20220120225051-c3fac7d636dd // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver v1.5.0 // indirect
 	github.com/Microsoft/go-winio v0.4.16 // indirect
 	github.com/PuerkitoBio/purell v1.1.1 // indirect
 	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
+	github.com/agext/levenshtein v1.2.1 // indirect
 	github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26 // indirect
+	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/c-robinson/iplib v1.0.3 // indirect
-	github.com/cespare/xxhash/v2 v2.1.1 // indirect
+	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/containerd/containerd v1.4.3 // indirect
-	github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
 	github.com/docker/distribution v2.7.1+incompatible // indirect
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-openapi/analysis v0.19.16 // indirect
+	github.com/go-openapi/inflect v0.19.0 // indirect
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
 	github.com/go-openapi/jsonreference v0.19.5 // indirect
 	github.com/go-openapi/loads v0.20.0 // indirect
@@ -90,8 +94,9 @@ require (
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/google/go-cmp v0.5.5 // indirect
+	github.com/google/go-cmp v0.5.6 // indirect
 	github.com/google/uuid v1.3.0 // indirect
+	github.com/hashicorp/hcl/v2 v2.10.0 // indirect
 	github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect
 	github.com/huandu/xstrings v1.3.2 // indirect
 	github.com/imdario/mergo v0.3.12 // indirect
@@ -110,14 +115,15 @@ require (
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 	github.com/leodido/go-urn v1.2.1 // indirect
 	github.com/mailru/easyjson v0.7.6 // indirect
-	github.com/mattn/go-colorable v0.1.9 // indirect
+	github.com/mattn/go-colorable v0.1.12 // indirect
 	github.com/mattn/go-isatty v0.0.14 // indirect
 	github.com/mattn/go-runewidth v0.0.10 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-testing-interface v1.0.0 // indirect
-	github.com/mitchellh/mapstructure v1.4.1 // indirect
+	github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
+	github.com/mitchellh/mapstructure v1.4.3 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -137,14 +143,17 @@ require (
 	github.com/ugorji/go/codec v1.2.6 // indirect
 	github.com/vjeantet/grok v1.0.1 // indirect
 	github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
+	github.com/zclconf/go-cty v1.8.0 // indirect
 	go.mongodb.org/mongo-driver v1.4.4 // indirect
 	golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
 	golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
 	golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
 	golang.org/x/text v0.3.7 // indirect
-	google.golang.org/appengine v1.6.6 // indirect
-	google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f // indirect
+	golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7 // indirect
+	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 438 - 0
go.sum


+ 53 - 3
pkg/apiserver/decisions_test.go

@@ -267,7 +267,7 @@ func TestGetDecision(t *testing.T) {
 	router.ServeHTTP(w, req)
 
 	assert.Equal(t, 200, w.Code)
-	assert.Contains(t, w.Body.String(), "\"id\":1,\"origin\":\"test\",\"scenario\":\"crowdsecurity/test\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"}]")
+	assert.Contains(t, w.Body.String(), "\"id\":3,\"origin\":\"test\",\"scenario\":\"crowdsecurity/test\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"}]")
 
 }
 
@@ -377,7 +377,7 @@ func TestDeleteDecision(t *testing.T) {
 	router.ServeHTTP(w, req)
 
 	assert.Equal(t, 200, w.Code)
-	assert.Equal(t, "{\"nbDeleted\":\"1\"}", w.Body.String())
+	assert.Equal(t, "{\"nbDeleted\":\"3\"}", w.Body.String())
 
 }
 
@@ -431,6 +431,56 @@ func TestStreamDecision(t *testing.T) {
 	req.Header.Add("X-Api-Key", APIKey)
 	router.ServeHTTP(w, req)
 
+	// the decision with id=3 is only returned because it's the longest decision
+	assert.Equal(t, 200, w.Code)
+	assert.Contains(t, w.Body.String(), "\"id\":3,\"origin\":\"test\",\"scenario\":\"crowdsecurity/test\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"}]}")
+	assert.NotContains(t, w.Body.String(), "\"id\":2")
+	assert.NotContains(t, w.Body.String(), "\"id\":1")
+	assert.Contains(t, w.Body.String(), "2h")
+
+	// id=3 decision is deleted, this won't affect `deleted`, because there are decisions
+	// targetting same IP
+	req, _ = http.NewRequest("DELETE", "/v1/decisions/3", strings.NewReader(""))
+	AddAuthHeaders(req, loginResp)
+	router.ServeHTTP(w, req)
+	assert.Equal(t, 200, w.Code)
+
+	w = httptest.NewRecorder()
+	req, _ = http.NewRequest("GET", "/v1/decisions/stream?startup=true", strings.NewReader(""))
+	req.Header.Add("X-Api-Key", APIKey)
+	router.ServeHTTP(w, req)
+
+	assert.Equal(t, 200, w.Code)
+	// the decision with id=2 is only returned because it's the longest decision
+	assert.Contains(t, w.Body.String(), "\"id\":2,\"origin\":\"test\",\"scenario\":\"crowdsecurity/test\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"}]}")
+	assert.NotContains(t, w.Body.String(), "\"id\":3")
+	assert.NotContains(t, w.Body.String(), "\"id\":1")
+	assert.Contains(t, w.Body.String(), "1h")
+	assert.Contains(t, w.Body.String(), "\"deleted\":null")
+
+	// We delete another decision, yet don't receive it in stream, since there's another decision on same IP
+	req, _ = http.NewRequest("DELETE", "/v1/decisions/2", strings.NewReader(""))
+	AddAuthHeaders(req, loginResp)
+	router.ServeHTTP(w, req)
+
+	w = httptest.NewRecorder()
+	req, _ = http.NewRequest("GET", "/v1/decisions/stream", strings.NewReader(""))
+	req.Header.Add("X-Api-Key", APIKey)
+	router.ServeHTTP(w, req)
+
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, "{\"deleted\":null,\"new\":null}", w.Body.String())
+
+	// Now all decisions for this IP are deleted, we should receive it in stream
+	req, _ = http.NewRequest("DELETE", "/v1/decisions/1", strings.NewReader(""))
+	AddAuthHeaders(req, loginResp)
+	router.ServeHTTP(w, req)
+
+	w = httptest.NewRecorder()
+	req, _ = http.NewRequest("GET", "/v1/decisions/stream", strings.NewReader(""))
+	req.Header.Add("X-Api-Key", APIKey)
+	router.ServeHTTP(w, req)
+
 	assert.Equal(t, 200, w.Code)
-	assert.Contains(t, w.Body.String(), "\"id\":1,\"origin\":\"test\",\"scenario\":\"crowdsecurity/test\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"}]}")
+	assert.NotContains(t, "\"deleted\":null", w.Body.String())
 }

+ 18 - 0
pkg/apiserver/tests/alert_sample.json

@@ -13,6 +13,24 @@
                 "scope": "Ip",
                 "value": "127.0.0.1",
                 "type": "ban"
+            },
+            {
+                "id": 2,
+                "duration": "2h",
+                "origin": "test",
+                "scenario": "crowdsecurity/test",
+                "scope": "Ip",
+                "value": "127.0.0.1",
+                "type": "ban"
+            },
+            {
+                "id": 3,
+                "duration": "3h",
+                "origin": "test",
+                "scenario": "crowdsecurity/test",
+                "scope": "Ip",
+                "value": "127.0.0.1",
+                "type": "ban"
             }
         ],
         "Events": [

+ 93 - 4
pkg/database/decisions.go

@@ -7,6 +7,7 @@ import (
 
 	"strconv"
 
+	"entgo.io/ent/dialect/sql"
 	"github.com/crowdsecurity/crowdsec/pkg/database/ent"
 	"github.com/crowdsecurity/crowdsec/pkg/database/ent/decision"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
@@ -170,8 +171,35 @@ func (c *Client) QueryDecisionWithFilter(filter map[string][]string) ([]*ent.Dec
 	return data, nil
 }
 
+// Groups by (decision.scope, decision.type, decision.value)
+func decisionGroupBy(s *sql.Selector) {
+	s.GroupBy(
+		decision.FieldScope,
+		decision.FieldType,
+		decision.FieldValue,
+	)
+}
+
+// Gets decisions where all (decision.scope, decision.type, decision.value) tuples are unique. 
+// The decision with maximum duration would be included if it's not expired.
 func (c *Client) QueryAllDecisionsWithFilters(filters map[string][]string) ([]*ent.Decision, error) {
-	query := c.Ent.Decision.Query().Where(decision.UntilGT(time.Now().UTC()))
+	decisionModifier := func(s *sql.Selector) {
+		decisionGroupBy(s)
+		s.Having(
+			sql.And(
+				sql.EQ(
+					decision.FieldUntil,
+					sql.Raw(sql.Max(decision.FieldUntil)),
+				),
+				sql.GT(
+					decision.FieldUntil,
+					time.Now().UTC(),
+				),
+			),
+		)
+	}
+	query := c.Ent.Decision.Query().Modify(decisionModifier).Where()
+
 	query, err := BuildDecisionRequestWithFilter(query, filters)
 
 	if err != nil {
@@ -188,7 +216,24 @@ func (c *Client) QueryAllDecisionsWithFilters(filters map[string][]string) ([]*e
 }
 
 func (c *Client) QueryExpiredDecisionsWithFilters(filters map[string][]string) ([]*ent.Decision, error) {
-	query := c.Ent.Decision.Query().Where(decision.UntilLT(time.Now().UTC()))
+
+	decisionModifier := func(s *sql.Selector) {
+		decisionGroupBy(s)
+		s.Having(
+			sql.And(
+				sql.EQ(
+					decision.FieldUntil,
+					sql.Raw(sql.Max(decision.FieldUntil)),
+				),
+				sql.LT(
+					decision.FieldUntil,
+					time.Now().UTC(),
+				),
+			),
+		)
+	}
+
+	query := c.Ent.Decision.Query().Modify(decisionModifier).Where()
 	query, err := BuildDecisionRequestWithFilter(query, filters)
 
 	if err != nil {
@@ -204,7 +249,32 @@ func (c *Client) QueryExpiredDecisionsWithFilters(filters map[string][]string) (
 }
 
 func (c *Client) QueryExpiredDecisionsSinceWithFilters(since time.Time, filters map[string][]string) ([]*ent.Decision, error) {
-	query := c.Ent.Decision.Query().Where(decision.UntilLT(time.Now().UTC())).Where(decision.UntilGT(since))
+	decisionModifier := func(s *sql.Selector) {
+		decisionGroupBy(s)
+		s.Having(
+			sql.And(
+				sql.EQ(
+					decision.FieldUntil,
+					sql.Raw(sql.Max(decision.FieldUntil)), // It has max duration
+				),
+				sql.GT(
+					decision.FieldUntil, // It was active at t=since
+					since,
+				),
+				sql.LT(
+					decision.FieldUntil, // It is expired as of now
+					time.Now().UTC(),
+				),
+			),
+		)
+	}
+
+	// This query returns 1 decision for each (decision.scope, decision.type, decision.value)
+	// if all decisions for the tuple are dead. Else it gives 0 decisions.
+	query := c.Ent.Decision.Query().Modify(
+		decisionModifier,
+	).Where()
+
 	query, err := BuildDecisionRequestWithFilter(query, filters)
 	if err != nil {
 		c.Log.Warningf("QueryExpiredDecisionsSinceWithFilters : %s", err)
@@ -221,7 +291,26 @@ func (c *Client) QueryExpiredDecisionsSinceWithFilters(since time.Time, filters
 }
 
 func (c *Client) QueryNewDecisionsSinceWithFilters(since time.Time, filters map[string][]string) ([]*ent.Decision, error) {
-	query := c.Ent.Decision.Query().Where(decision.CreatedAtGT(since)).Where(decision.UntilGT(time.Now().UTC()))
+	decisionModifier := func(s *sql.Selector) {
+		decisionGroupBy(s)
+		s.Having(
+			sql.And(
+				sql.EQ(
+					decision.FieldUntil,
+					sql.Raw(sql.Max(decision.FieldUntil)),
+				),
+				sql.GT(
+					decision.FieldCreatedAt,
+					since,
+				),
+				sql.GT(
+					decision.FieldUntil,
+					time.Now().UTC(),
+				),
+			),
+		)
+	}
+	query := c.Ent.Decision.Query().Modify(decisionModifier).Where()
 	query, err := BuildDecisionRequestWithFilter(query, filters)
 	if err != nil {
 		c.Log.Warningf("QueryNewDecisionsSinceWithFilters : %s", err)

+ 2 - 2
pkg/database/ent/alert_create.go

@@ -496,10 +496,10 @@ func (ac *AlertCreate) defaults() {
 // check runs all checks and user-defined validators on the builder.
 func (ac *AlertCreate) check() error {
 	if _, ok := ac.mutation.Scenario(); !ok {
-		return &ValidationError{Name: "scenario", err: errors.New(`ent: missing required field "scenario"`)}
+		return &ValidationError{Name: "scenario", err: errors.New(`ent: missing required field "Alert.scenario"`)}
 	}
 	if _, ok := ac.mutation.Simulated(); !ok {
-		return &ValidationError{Name: "simulated", err: errors.New(`ent: missing required field "simulated"`)}
+		return &ValidationError{Name: "simulated", err: errors.New(`ent: missing required field "Alert.simulated"`)}
 	}
 	return nil
 }

+ 30 - 3
pkg/database/ent/alert_query.go

@@ -35,6 +35,7 @@ type AlertQuery struct {
 	withEvents    *EventQuery
 	withMetas     *MetaQuery
 	withFKs       bool
+	modifiers     []func(s *sql.Selector)
 	// intermediate query (i.e. traversal path).
 	sql  *sql.Selector
 	path func(context.Context) (*sql.Selector, error)
@@ -486,6 +487,9 @@ func (aq *AlertQuery) sqlAll(ctx context.Context) ([]*Alert, error) {
 		node.Edges.loadedTypes = loadedTypes
 		return node.assignValues(columns, values)
 	}
+	if len(aq.modifiers) > 0 {
+		_spec.Modifiers = aq.modifiers
+	}
 	if err := sqlgraph.QueryNodes(ctx, aq.driver, _spec); err != nil {
 		return nil, err
 	}
@@ -614,6 +618,13 @@ func (aq *AlertQuery) sqlAll(ctx context.Context) ([]*Alert, error) {
 
 func (aq *AlertQuery) sqlCount(ctx context.Context) (int, error) {
 	_spec := aq.querySpec()
+	if len(aq.modifiers) > 0 {
+		_spec.Modifiers = aq.modifiers
+	}
+	_spec.Node.Columns = aq.fields
+	if len(aq.fields) > 0 {
+		_spec.Unique = aq.unique != nil && *aq.unique
+	}
 	return sqlgraph.CountNodes(ctx, aq.driver, _spec)
 }
 
@@ -685,6 +696,12 @@ func (aq *AlertQuery) sqlQuery(ctx context.Context) *sql.Selector {
 		selector = aq.sql
 		selector.Select(selector.Columns(columns...)...)
 	}
+	if aq.unique != nil && *aq.unique {
+		selector.Distinct()
+	}
+	for _, m := range aq.modifiers {
+		m(selector)
+	}
 	for _, p := range aq.predicates {
 		p(selector)
 	}
@@ -702,6 +719,12 @@ func (aq *AlertQuery) sqlQuery(ctx context.Context) *sql.Selector {
 	return selector
 }
 
+// Modify adds a query modifier for attaching custom logic to queries.
+func (aq *AlertQuery) Modify(modifiers ...func(s *sql.Selector)) *AlertSelect {
+	aq.modifiers = append(aq.modifiers, modifiers...)
+	return aq.Select()
+}
+
 // AlertGroupBy is the group-by builder for Alert entities.
 type AlertGroupBy struct {
 	config
@@ -963,9 +986,7 @@ func (agb *AlertGroupBy) sqlQuery() *sql.Selector {
 		for _, f := range agb.fields {
 			columns = append(columns, selector.C(f))
 		}
-		for _, c := range aggregation {
-			columns = append(columns, c)
-		}
+		columns = append(columns, aggregation...)
 		selector.Select(columns...)
 	}
 	return selector.GroupBy(selector.Columns(agb.fields...)...)
@@ -1191,3 +1212,9 @@ func (as *AlertSelect) sqlScan(ctx context.Context, v interface{}) error {
 	defer rows.Close()
 	return sql.ScanSlice(rows, v)
 }
+
+// Modify adds a query modifier for attaching custom logic to queries.
+func (as *AlertSelect) Modify(modifiers ...func(s *sql.Selector)) *AlertSelect {
+	as.modifiers = append(as.modifiers, modifiers...)
+	return as
+}

+ 2 - 1
pkg/database/ent/alert_update.go

@@ -4,6 +4,7 @@ package ent
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"time"
 
@@ -1861,7 +1862,7 @@ func (auo *AlertUpdateOne) sqlSave(ctx context.Context) (_node *Alert, err error
 	}
 	id, ok := auo.mutation.ID()
 	if !ok {
-		return nil, &ValidationError{Name: "ID", err: fmt.Errorf("missing Alert.ID for update")}
+		return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Alert.id" for update`)}
 	}
 	_spec.Node.ID.Value = id
 	if fields := auo.fields; len(fields) > 0 {

+ 4 - 4
pkg/database/ent/bouncer_create.go

@@ -232,16 +232,16 @@ func (bc *BouncerCreate) defaults() {
 // check runs all checks and user-defined validators on the builder.
 func (bc *BouncerCreate) check() error {
 	if _, ok := bc.mutation.Name(); !ok {
-		return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "name"`)}
+		return &ValidationError{Name: "name", err: errors.New(`ent: missing required field "Bouncer.name"`)}
 	}
 	if _, ok := bc.mutation.APIKey(); !ok {
-		return &ValidationError{Name: "api_key", err: errors.New(`ent: missing required field "api_key"`)}
+		return &ValidationError{Name: "api_key", err: errors.New(`ent: missing required field "Bouncer.api_key"`)}
 	}
 	if _, ok := bc.mutation.Revoked(); !ok {
-		return &ValidationError{Name: "revoked", err: errors.New(`ent: missing required field "revoked"`)}
+		return &ValidationError{Name: "revoked", err: errors.New(`ent: missing required field "Bouncer.revoked"`)}
 	}
 	if _, ok := bc.mutation.LastPull(); !ok {
-		return &ValidationError{Name: "last_pull", err: errors.New(`ent: missing required field "last_pull"`)}
+		return &ValidationError{Name: "last_pull", err: errors.New(`ent: missing required field "Bouncer.last_pull"`)}
 	}
 	return nil
 }

+ 30 - 3
pkg/database/ent/bouncer_query.go

@@ -24,6 +24,7 @@ type BouncerQuery struct {
 	order      []OrderFunc
 	fields     []string
 	predicates []predicate.Bouncer
+	modifiers  []func(s *sql.Selector)
 	// intermediate query (i.e. traversal path).
 	sql  *sql.Selector
 	path func(context.Context) (*sql.Selector, error)
@@ -325,6 +326,9 @@ func (bq *BouncerQuery) sqlAll(ctx context.Context) ([]*Bouncer, error) {
 		node := nodes[len(nodes)-1]
 		return node.assignValues(columns, values)
 	}
+	if len(bq.modifiers) > 0 {
+		_spec.Modifiers = bq.modifiers
+	}
 	if err := sqlgraph.QueryNodes(ctx, bq.driver, _spec); err != nil {
 		return nil, err
 	}
@@ -336,6 +340,13 @@ func (bq *BouncerQuery) sqlAll(ctx context.Context) ([]*Bouncer, error) {
 
 func (bq *BouncerQuery) sqlCount(ctx context.Context) (int, error) {
 	_spec := bq.querySpec()
+	if len(bq.modifiers) > 0 {
+		_spec.Modifiers = bq.modifiers
+	}
+	_spec.Node.Columns = bq.fields
+	if len(bq.fields) > 0 {
+		_spec.Unique = bq.unique != nil && *bq.unique
+	}
 	return sqlgraph.CountNodes(ctx, bq.driver, _spec)
 }
 
@@ -407,6 +418,12 @@ func (bq *BouncerQuery) sqlQuery(ctx context.Context) *sql.Selector {
 		selector = bq.sql
 		selector.Select(selector.Columns(columns...)...)
 	}
+	if bq.unique != nil && *bq.unique {
+		selector.Distinct()
+	}
+	for _, m := range bq.modifiers {
+		m(selector)
+	}
 	for _, p := range bq.predicates {
 		p(selector)
 	}
@@ -424,6 +441,12 @@ func (bq *BouncerQuery) sqlQuery(ctx context.Context) *sql.Selector {
 	return selector
 }
 
+// Modify adds a query modifier for attaching custom logic to queries.
+func (bq *BouncerQuery) Modify(modifiers ...func(s *sql.Selector)) *BouncerSelect {
+	bq.modifiers = append(bq.modifiers, modifiers...)
+	return bq.Select()
+}
+
 // BouncerGroupBy is the group-by builder for Bouncer entities.
 type BouncerGroupBy struct {
 	config
@@ -685,9 +708,7 @@ func (bgb *BouncerGroupBy) sqlQuery() *sql.Selector {
 		for _, f := range bgb.fields {
 			columns = append(columns, selector.C(f))
 		}
-		for _, c := range aggregation {
-			columns = append(columns, c)
-		}
+		columns = append(columns, aggregation...)
 		selector.Select(columns...)
 	}
 	return selector.GroupBy(selector.Columns(bgb.fields...)...)
@@ -913,3 +934,9 @@ func (bs *BouncerSelect) sqlScan(ctx context.Context, v interface{}) error {
 	defer rows.Close()
 	return sql.ScanSlice(rows, v)
 }
+
+// Modify adds a query modifier for attaching custom logic to queries.
+func (bs *BouncerSelect) Modify(modifiers ...func(s *sql.Selector)) *BouncerSelect {
+	bs.modifiers = append(bs.modifiers, modifiers...)
+	return bs
+}

+ 2 - 1
pkg/database/ent/bouncer_update.go

@@ -4,6 +4,7 @@ package ent
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"time"
 
@@ -606,7 +607,7 @@ func (buo *BouncerUpdateOne) sqlSave(ctx context.Context) (_node *Bouncer, err e
 	}
 	id, ok := buo.mutation.ID()
 	if !ok {
-		return nil, &ValidationError{Name: "ID", err: fmt.Errorf("missing Bouncer.ID for update")}
+		return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Bouncer.id" for update`)}
 	}
 	_spec.Node.ID.Value = id
 	if fields := buo.fields; len(fields) > 0 {

+ 1 - 0
pkg/database/ent/client.go

@@ -113,6 +113,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
 	cfg := c.config
 	cfg.driver = &txDriver{tx: tx, drv: c.driver}
 	return &Tx{
+		ctx:      ctx,
 		config:   cfg,
 		Alert:    NewAlertClient(cfg),
 		Bouncer:  NewBouncerClient(cfg),

+ 7 - 7
pkg/database/ent/decision_create.go

@@ -276,25 +276,25 @@ func (dc *DecisionCreate) defaults() {
 // check runs all checks and user-defined validators on the builder.
 func (dc *DecisionCreate) check() error {
 	if _, ok := dc.mutation.Until(); !ok {
-		return &ValidationError{Name: "until", err: errors.New(`ent: missing required field "until"`)}
+		return &ValidationError{Name: "until", err: errors.New(`ent: missing required field "Decision.until"`)}
 	}
 	if _, ok := dc.mutation.Scenario(); !ok {
-		return &ValidationError{Name: "scenario", err: errors.New(`ent: missing required field "scenario"`)}
+		return &ValidationError{Name: "scenario", err: errors.New(`ent: missing required field "Decision.scenario"`)}
 	}
 	if _, ok := dc.mutation.GetType(); !ok {
-		return &ValidationError{Name: "type", err: errors.New(`ent: missing required field "type"`)}
+		return &ValidationError{Name: "type", err: errors.New(`ent: missing required field "Decision.type"`)}
 	}
 	if _, ok := dc.mutation.Scope(); !ok {
-		return &ValidationError{Name: "scope", err: errors.New(`ent: missing required field "scope"`)}
+		return &ValidationError{Name: "scope", err: errors.New(`ent: missing required field "Decision.scope"`)}
 	}
 	if _, ok := dc.mutation.Value(); !ok {
-		return &ValidationError{Name: "value", err: errors.New(`ent: missing required field "value"`)}
+		return &ValidationError{Name: "value", err: errors.New(`ent: missing required field "Decision.value"`)}
 	}
 	if _, ok := dc.mutation.Origin(); !ok {
-		return &ValidationError{Name: "origin", err: errors.New(`ent: missing required field "origin"`)}
+		return &ValidationError{Name: "origin", err: errors.New(`ent: missing required field "Decision.origin"`)}
 	}
 	if _, ok := dc.mutation.Simulated(); !ok {
-		return &ValidationError{Name: "simulated", err: errors.New(`ent: missing required field "simulated"`)}
+		return &ValidationError{Name: "simulated", err: errors.New(`ent: missing required field "Decision.simulated"`)}
 	}
 	return nil
 }

+ 30 - 3
pkg/database/ent/decision_query.go

@@ -28,6 +28,7 @@ type DecisionQuery struct {
 	// eager-loading edges.
 	withOwner *AlertQuery
 	withFKs   bool
+	modifiers []func(s *sql.Selector)
 	// intermediate query (i.e. traversal path).
 	sql  *sql.Selector
 	path func(context.Context) (*sql.Selector, error)
@@ -374,6 +375,9 @@ func (dq *DecisionQuery) sqlAll(ctx context.Context) ([]*Decision, error) {
 		node.Edges.loadedTypes = loadedTypes
 		return node.assignValues(columns, values)
 	}
+	if len(dq.modifiers) > 0 {
+		_spec.Modifiers = dq.modifiers
+	}
 	if err := sqlgraph.QueryNodes(ctx, dq.driver, _spec); err != nil {
 		return nil, err
 	}
@@ -415,6 +419,13 @@ func (dq *DecisionQuery) sqlAll(ctx context.Context) ([]*Decision, error) {
 
 func (dq *DecisionQuery) sqlCount(ctx context.Context) (int, error) {
 	_spec := dq.querySpec()
+	if len(dq.modifiers) > 0 {
+		_spec.Modifiers = dq.modifiers
+	}
+	_spec.Node.Columns = dq.fields
+	if len(dq.fields) > 0 {
+		_spec.Unique = dq.unique != nil && *dq.unique
+	}
 	return sqlgraph.CountNodes(ctx, dq.driver, _spec)
 }
 
@@ -486,6 +497,12 @@ func (dq *DecisionQuery) sqlQuery(ctx context.Context) *sql.Selector {
 		selector = dq.sql
 		selector.Select(selector.Columns(columns...)...)
 	}
+	if dq.unique != nil && *dq.unique {
+		selector.Distinct()
+	}
+	for _, m := range dq.modifiers {
+		m(selector)
+	}
 	for _, p := range dq.predicates {
 		p(selector)
 	}
@@ -503,6 +520,12 @@ func (dq *DecisionQuery) sqlQuery(ctx context.Context) *sql.Selector {
 	return selector
 }
 
+// Modify adds a query modifier for attaching custom logic to queries.
+func (dq *DecisionQuery) Modify(modifiers ...func(s *sql.Selector)) *DecisionSelect {
+	dq.modifiers = append(dq.modifiers, modifiers...)
+	return dq.Select()
+}
+
 // DecisionGroupBy is the group-by builder for Decision entities.
 type DecisionGroupBy struct {
 	config
@@ -764,9 +787,7 @@ func (dgb *DecisionGroupBy) sqlQuery() *sql.Selector {
 		for _, f := range dgb.fields {
 			columns = append(columns, selector.C(f))
 		}
-		for _, c := range aggregation {
-			columns = append(columns, c)
-		}
+		columns = append(columns, aggregation...)
 		selector.Select(columns...)
 	}
 	return selector.GroupBy(selector.Columns(dgb.fields...)...)
@@ -992,3 +1013,9 @@ func (ds *DecisionSelect) sqlScan(ctx context.Context, v interface{}) error {
 	defer rows.Close()
 	return sql.ScanSlice(rows, v)
 }
+
+// Modify adds a query modifier for attaching custom logic to queries.
+func (ds *DecisionSelect) Modify(modifiers ...func(s *sql.Selector)) *DecisionSelect {
+	ds.modifiers = append(ds.modifiers, modifiers...)
+	return ds
+}

+ 2 - 1
pkg/database/ent/decision_update.go

@@ -4,6 +4,7 @@ package ent
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"time"
 
@@ -907,7 +908,7 @@ func (duo *DecisionUpdateOne) sqlSave(ctx context.Context) (_node *Decision, err
 	}
 	id, ok := duo.mutation.ID()
 	if !ok {
-		return nil, &ValidationError{Name: "ID", err: fmt.Errorf("missing Decision.ID for update")}
+		return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Decision.id" for update`)}
 	}
 	_spec.Node.ID.Value = id
 	if fields := duo.fields; len(fields) > 0 {

+ 1 - 1
pkg/database/ent/ent.go

@@ -151,7 +151,7 @@ func Sum(field string) AggregateFunc {
 	}
 }
 
-// ValidationError returns when validating a field fails.
+// ValidationError returns when validating a field or edge fails.
 type ValidationError struct {
 	Name string // Field or edge name.
 	err  error

+ 3 - 3
pkg/database/ent/event_create.go

@@ -164,14 +164,14 @@ func (ec *EventCreate) defaults() {
 // check runs all checks and user-defined validators on the builder.
 func (ec *EventCreate) check() error {
 	if _, ok := ec.mutation.Time(); !ok {
-		return &ValidationError{Name: "time", err: errors.New(`ent: missing required field "time"`)}
+		return &ValidationError{Name: "time", err: errors.New(`ent: missing required field "Event.time"`)}
 	}
 	if _, ok := ec.mutation.Serialized(); !ok {
-		return &ValidationError{Name: "serialized", err: errors.New(`ent: missing required field "serialized"`)}
+		return &ValidationError{Name: "serialized", err: errors.New(`ent: missing required field "Event.serialized"`)}
 	}
 	if v, ok := ec.mutation.Serialized(); ok {
 		if err := event.SerializedValidator(v); err != nil {
-			return &ValidationError{Name: "serialized", err: fmt.Errorf(`ent: validator failed for field "serialized": %w`, err)}
+			return &ValidationError{Name: "serialized", err: fmt.Errorf(`ent: validator failed for field "Event.serialized": %w`, err)}
 		}
 	}
 	return nil

+ 30 - 3
pkg/database/ent/event_query.go

@@ -28,6 +28,7 @@ type EventQuery struct {
 	// eager-loading edges.
 	withOwner *AlertQuery
 	withFKs   bool
+	modifiers []func(s *sql.Selector)
 	// intermediate query (i.e. traversal path).
 	sql  *sql.Selector
 	path func(context.Context) (*sql.Selector, error)
@@ -374,6 +375,9 @@ func (eq *EventQuery) sqlAll(ctx context.Context) ([]*Event, error) {
 		node.Edges.loadedTypes = loadedTypes
 		return node.assignValues(columns, values)
 	}
+	if len(eq.modifiers) > 0 {
+		_spec.Modifiers = eq.modifiers
+	}
 	if err := sqlgraph.QueryNodes(ctx, eq.driver, _spec); err != nil {
 		return nil, err
 	}
@@ -415,6 +419,13 @@ func (eq *EventQuery) sqlAll(ctx context.Context) ([]*Event, error) {
 
 func (eq *EventQuery) sqlCount(ctx context.Context) (int, error) {
 	_spec := eq.querySpec()
+	if len(eq.modifiers) > 0 {
+		_spec.Modifiers = eq.modifiers
+	}
+	_spec.Node.Columns = eq.fields
+	if len(eq.fields) > 0 {
+		_spec.Unique = eq.unique != nil && *eq.unique
+	}
 	return sqlgraph.CountNodes(ctx, eq.driver, _spec)
 }
 
@@ -486,6 +497,12 @@ func (eq *EventQuery) sqlQuery(ctx context.Context) *sql.Selector {
 		selector = eq.sql
 		selector.Select(selector.Columns(columns...)...)
 	}
+	if eq.unique != nil && *eq.unique {
+		selector.Distinct()
+	}
+	for _, m := range eq.modifiers {
+		m(selector)
+	}
 	for _, p := range eq.predicates {
 		p(selector)
 	}
@@ -503,6 +520,12 @@ func (eq *EventQuery) sqlQuery(ctx context.Context) *sql.Selector {
 	return selector
 }
 
+// Modify adds a query modifier for attaching custom logic to queries.
+func (eq *EventQuery) Modify(modifiers ...func(s *sql.Selector)) *EventSelect {
+	eq.modifiers = append(eq.modifiers, modifiers...)
+	return eq.Select()
+}
+
 // EventGroupBy is the group-by builder for Event entities.
 type EventGroupBy struct {
 	config
@@ -764,9 +787,7 @@ func (egb *EventGroupBy) sqlQuery() *sql.Selector {
 		for _, f := range egb.fields {
 			columns = append(columns, selector.C(f))
 		}
-		for _, c := range aggregation {
-			columns = append(columns, c)
-		}
+		columns = append(columns, aggregation...)
 		selector.Select(columns...)
 	}
 	return selector.GroupBy(selector.Columns(egb.fields...)...)
@@ -992,3 +1013,9 @@ func (es *EventSelect) sqlScan(ctx context.Context, v interface{}) error {
 	defer rows.Close()
 	return sql.ScanSlice(rows, v)
 }
+
+// Modify adds a query modifier for attaching custom logic to queries.
+func (es *EventSelect) Modify(modifiers ...func(s *sql.Selector)) *EventSelect {
+	es.modifiers = append(es.modifiers, modifiers...)
+	return es
+}

+ 4 - 3
pkg/database/ent/event_update.go

@@ -4,6 +4,7 @@ package ent
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"time"
 
@@ -171,7 +172,7 @@ func (eu *EventUpdate) defaults() {
 func (eu *EventUpdate) check() error {
 	if v, ok := eu.mutation.Serialized(); ok {
 		if err := event.SerializedValidator(v); err != nil {
-			return &ValidationError{Name: "serialized", err: fmt.Errorf("ent: validator failed for field \"serialized\": %w", err)}
+			return &ValidationError{Name: "serialized", err: fmt.Errorf(`ent: validator failed for field "Event.serialized": %w`, err)}
 		}
 	}
 	return nil
@@ -439,7 +440,7 @@ func (euo *EventUpdateOne) defaults() {
 func (euo *EventUpdateOne) check() error {
 	if v, ok := euo.mutation.Serialized(); ok {
 		if err := event.SerializedValidator(v); err != nil {
-			return &ValidationError{Name: "serialized", err: fmt.Errorf("ent: validator failed for field \"serialized\": %w", err)}
+			return &ValidationError{Name: "serialized", err: fmt.Errorf(`ent: validator failed for field "Event.serialized": %w`, err)}
 		}
 	}
 	return nil
@@ -458,7 +459,7 @@ func (euo *EventUpdateOne) sqlSave(ctx context.Context) (_node *Event, err error
 	}
 	id, ok := euo.mutation.ID()
 	if !ok {
-		return nil, &ValidationError{Name: "ID", err: fmt.Errorf("missing Event.ID for update")}
+		return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Event.id" for update`)}
 	}
 	_spec.Node.ID.Value = id
 	if fields := euo.fields; len(fields) > 0 {

+ 5 - 5
pkg/database/ent/machine_create.go

@@ -244,21 +244,21 @@ func (mc *MachineCreate) defaults() {
 // check runs all checks and user-defined validators on the builder.
 func (mc *MachineCreate) check() error {
 	if _, ok := mc.mutation.MachineId(); !ok {
-		return &ValidationError{Name: "machineId", err: errors.New(`ent: missing required field "machineId"`)}
+		return &ValidationError{Name: "machineId", err: errors.New(`ent: missing required field "Machine.machineId"`)}
 	}
 	if _, ok := mc.mutation.Password(); !ok {
-		return &ValidationError{Name: "password", err: errors.New(`ent: missing required field "password"`)}
+		return &ValidationError{Name: "password", err: errors.New(`ent: missing required field "Machine.password"`)}
 	}
 	if _, ok := mc.mutation.IpAddress(); !ok {
-		return &ValidationError{Name: "ipAddress", err: errors.New(`ent: missing required field "ipAddress"`)}
+		return &ValidationError{Name: "ipAddress", err: errors.New(`ent: missing required field "Machine.ipAddress"`)}
 	}
 	if v, ok := mc.mutation.Scenarios(); ok {
 		if err := machine.ScenariosValidator(v); err != nil {
-			return &ValidationError{Name: "scenarios", err: fmt.Errorf(`ent: validator failed for field "scenarios": %w`, err)}
+			return &ValidationError{Name: "scenarios", err: fmt.Errorf(`ent: validator failed for field "Machine.scenarios": %w`, err)}
 		}
 	}
 	if _, ok := mc.mutation.IsValidated(); !ok {
-		return &ValidationError{Name: "isValidated", err: errors.New(`ent: missing required field "isValidated"`)}
+		return &ValidationError{Name: "isValidated", err: errors.New(`ent: missing required field "Machine.isValidated"`)}
 	}
 	return nil
 }

+ 30 - 3
pkg/database/ent/machine_query.go

@@ -28,6 +28,7 @@ type MachineQuery struct {
 	predicates []predicate.Machine
 	// eager-loading edges.
 	withAlerts *AlertQuery
+	modifiers  []func(s *sql.Selector)
 	// intermediate query (i.e. traversal path).
 	sql  *sql.Selector
 	path func(context.Context) (*sql.Selector, error)
@@ -367,6 +368,9 @@ func (mq *MachineQuery) sqlAll(ctx context.Context) ([]*Machine, error) {
 		node.Edges.loadedTypes = loadedTypes
 		return node.assignValues(columns, values)
 	}
+	if len(mq.modifiers) > 0 {
+		_spec.Modifiers = mq.modifiers
+	}
 	if err := sqlgraph.QueryNodes(ctx, mq.driver, _spec); err != nil {
 		return nil, err
 	}
@@ -408,6 +412,13 @@ func (mq *MachineQuery) sqlAll(ctx context.Context) ([]*Machine, error) {
 
 func (mq *MachineQuery) sqlCount(ctx context.Context) (int, error) {
 	_spec := mq.querySpec()
+	if len(mq.modifiers) > 0 {
+		_spec.Modifiers = mq.modifiers
+	}
+	_spec.Node.Columns = mq.fields
+	if len(mq.fields) > 0 {
+		_spec.Unique = mq.unique != nil && *mq.unique
+	}
 	return sqlgraph.CountNodes(ctx, mq.driver, _spec)
 }
 
@@ -479,6 +490,12 @@ func (mq *MachineQuery) sqlQuery(ctx context.Context) *sql.Selector {
 		selector = mq.sql
 		selector.Select(selector.Columns(columns...)...)
 	}
+	if mq.unique != nil && *mq.unique {
+		selector.Distinct()
+	}
+	for _, m := range mq.modifiers {
+		m(selector)
+	}
 	for _, p := range mq.predicates {
 		p(selector)
 	}
@@ -496,6 +513,12 @@ func (mq *MachineQuery) sqlQuery(ctx context.Context) *sql.Selector {
 	return selector
 }
 
+// Modify adds a query modifier for attaching custom logic to queries.
+func (mq *MachineQuery) Modify(modifiers ...func(s *sql.Selector)) *MachineSelect {
+	mq.modifiers = append(mq.modifiers, modifiers...)
+	return mq.Select()
+}
+
 // MachineGroupBy is the group-by builder for Machine entities.
 type MachineGroupBy struct {
 	config
@@ -757,9 +780,7 @@ func (mgb *MachineGroupBy) sqlQuery() *sql.Selector {
 		for _, f := range mgb.fields {
 			columns = append(columns, selector.C(f))
 		}
-		for _, c := range aggregation {
-			columns = append(columns, c)
-		}
+		columns = append(columns, aggregation...)
 		selector.Select(columns...)
 	}
 	return selector.GroupBy(selector.Columns(mgb.fields...)...)
@@ -985,3 +1006,9 @@ func (ms *MachineSelect) sqlScan(ctx context.Context, v interface{}) error {
 	defer rows.Close()
 	return sql.ScanSlice(rows, v)
 }
+
+// Modify adds a query modifier for attaching custom logic to queries.
+func (ms *MachineSelect) Modify(modifiers ...func(s *sql.Selector)) *MachineSelect {
+	ms.modifiers = append(ms.modifiers, modifiers...)
+	return ms
+}

+ 4 - 3
pkg/database/ent/machine_update.go

@@ -4,6 +4,7 @@ package ent
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"time"
 
@@ -278,7 +279,7 @@ func (mu *MachineUpdate) defaults() {
 func (mu *MachineUpdate) check() error {
 	if v, ok := mu.mutation.Scenarios(); ok {
 		if err := machine.ScenariosValidator(v); err != nil {
-			return &ValidationError{Name: "scenarios", err: fmt.Errorf("ent: validator failed for field \"scenarios\": %w", err)}
+			return &ValidationError{Name: "scenarios", err: fmt.Errorf(`ent: validator failed for field "Machine.scenarios": %w`, err)}
 		}
 	}
 	return nil
@@ -738,7 +739,7 @@ func (muo *MachineUpdateOne) defaults() {
 func (muo *MachineUpdateOne) check() error {
 	if v, ok := muo.mutation.Scenarios(); ok {
 		if err := machine.ScenariosValidator(v); err != nil {
-			return &ValidationError{Name: "scenarios", err: fmt.Errorf("ent: validator failed for field \"scenarios\": %w", err)}
+			return &ValidationError{Name: "scenarios", err: fmt.Errorf(`ent: validator failed for field "Machine.scenarios": %w`, err)}
 		}
 	}
 	return nil
@@ -757,7 +758,7 @@ func (muo *MachineUpdateOne) sqlSave(ctx context.Context) (_node *Machine, err e
 	}
 	id, ok := muo.mutation.ID()
 	if !ok {
-		return nil, &ValidationError{Name: "ID", err: fmt.Errorf("missing Machine.ID for update")}
+		return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Machine.id" for update`)}
 	}
 	_spec.Node.ID.Value = id
 	if fields := muo.fields; len(fields) > 0 {

+ 3 - 3
pkg/database/ent/meta_create.go

@@ -164,14 +164,14 @@ func (mc *MetaCreate) defaults() {
 // check runs all checks and user-defined validators on the builder.
 func (mc *MetaCreate) check() error {
 	if _, ok := mc.mutation.Key(); !ok {
-		return &ValidationError{Name: "key", err: errors.New(`ent: missing required field "key"`)}
+		return &ValidationError{Name: "key", err: errors.New(`ent: missing required field "Meta.key"`)}
 	}
 	if _, ok := mc.mutation.Value(); !ok {
-		return &ValidationError{Name: "value", err: errors.New(`ent: missing required field "value"`)}
+		return &ValidationError{Name: "value", err: errors.New(`ent: missing required field "Meta.value"`)}
 	}
 	if v, ok := mc.mutation.Value(); ok {
 		if err := meta.ValueValidator(v); err != nil {
-			return &ValidationError{Name: "value", err: fmt.Errorf(`ent: validator failed for field "value": %w`, err)}
+			return &ValidationError{Name: "value", err: fmt.Errorf(`ent: validator failed for field "Meta.value": %w`, err)}
 		}
 	}
 	return nil

+ 30 - 3
pkg/database/ent/meta_query.go

@@ -28,6 +28,7 @@ type MetaQuery struct {
 	// eager-loading edges.
 	withOwner *AlertQuery
 	withFKs   bool
+	modifiers []func(s *sql.Selector)
 	// intermediate query (i.e. traversal path).
 	sql  *sql.Selector
 	path func(context.Context) (*sql.Selector, error)
@@ -374,6 +375,9 @@ func (mq *MetaQuery) sqlAll(ctx context.Context) ([]*Meta, error) {
 		node.Edges.loadedTypes = loadedTypes
 		return node.assignValues(columns, values)
 	}
+	if len(mq.modifiers) > 0 {
+		_spec.Modifiers = mq.modifiers
+	}
 	if err := sqlgraph.QueryNodes(ctx, mq.driver, _spec); err != nil {
 		return nil, err
 	}
@@ -415,6 +419,13 @@ func (mq *MetaQuery) sqlAll(ctx context.Context) ([]*Meta, error) {
 
 func (mq *MetaQuery) sqlCount(ctx context.Context) (int, error) {
 	_spec := mq.querySpec()
+	if len(mq.modifiers) > 0 {
+		_spec.Modifiers = mq.modifiers
+	}
+	_spec.Node.Columns = mq.fields
+	if len(mq.fields) > 0 {
+		_spec.Unique = mq.unique != nil && *mq.unique
+	}
 	return sqlgraph.CountNodes(ctx, mq.driver, _spec)
 }
 
@@ -486,6 +497,12 @@ func (mq *MetaQuery) sqlQuery(ctx context.Context) *sql.Selector {
 		selector = mq.sql
 		selector.Select(selector.Columns(columns...)...)
 	}
+	if mq.unique != nil && *mq.unique {
+		selector.Distinct()
+	}
+	for _, m := range mq.modifiers {
+		m(selector)
+	}
 	for _, p := range mq.predicates {
 		p(selector)
 	}
@@ -503,6 +520,12 @@ func (mq *MetaQuery) sqlQuery(ctx context.Context) *sql.Selector {
 	return selector
 }
 
+// Modify adds a query modifier for attaching custom logic to queries.
+func (mq *MetaQuery) Modify(modifiers ...func(s *sql.Selector)) *MetaSelect {
+	mq.modifiers = append(mq.modifiers, modifiers...)
+	return mq.Select()
+}
+
 // MetaGroupBy is the group-by builder for Meta entities.
 type MetaGroupBy struct {
 	config
@@ -764,9 +787,7 @@ func (mgb *MetaGroupBy) sqlQuery() *sql.Selector {
 		for _, f := range mgb.fields {
 			columns = append(columns, selector.C(f))
 		}
-		for _, c := range aggregation {
-			columns = append(columns, c)
-		}
+		columns = append(columns, aggregation...)
 		selector.Select(columns...)
 	}
 	return selector.GroupBy(selector.Columns(mgb.fields...)...)
@@ -992,3 +1013,9 @@ func (ms *MetaSelect) sqlScan(ctx context.Context, v interface{}) error {
 	defer rows.Close()
 	return sql.ScanSlice(rows, v)
 }
+
+// Modify adds a query modifier for attaching custom logic to queries.
+func (ms *MetaSelect) Modify(modifiers ...func(s *sql.Selector)) *MetaSelect {
+	ms.modifiers = append(ms.modifiers, modifiers...)
+	return ms
+}

+ 4 - 3
pkg/database/ent/meta_update.go

@@ -4,6 +4,7 @@ package ent
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"time"
 
@@ -171,7 +172,7 @@ func (mu *MetaUpdate) defaults() {
 func (mu *MetaUpdate) check() error {
 	if v, ok := mu.mutation.Value(); ok {
 		if err := meta.ValueValidator(v); err != nil {
-			return &ValidationError{Name: "value", err: fmt.Errorf("ent: validator failed for field \"value\": %w", err)}
+			return &ValidationError{Name: "value", err: fmt.Errorf(`ent: validator failed for field "Meta.value": %w`, err)}
 		}
 	}
 	return nil
@@ -439,7 +440,7 @@ func (muo *MetaUpdateOne) defaults() {
 func (muo *MetaUpdateOne) check() error {
 	if v, ok := muo.mutation.Value(); ok {
 		if err := meta.ValueValidator(v); err != nil {
-			return &ValidationError{Name: "value", err: fmt.Errorf("ent: validator failed for field \"value\": %w", err)}
+			return &ValidationError{Name: "value", err: fmt.Errorf(`ent: validator failed for field "Meta.value": %w`, err)}
 		}
 	}
 	return nil
@@ -458,7 +459,7 @@ func (muo *MetaUpdateOne) sqlSave(ctx context.Context) (_node *Meta, err error)
 	}
 	id, ok := muo.mutation.ID()
 	if !ok {
-		return nil, &ValidationError{Name: "ID", err: fmt.Errorf("missing Meta.ID for update")}
+		return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Meta.id" for update`)}
 	}
 	_spec.Node.ID.Value = id
 	if fields := muo.fields; len(fields) > 0 {

+ 1 - 2
pkg/database/ent/migrate/migrate.go

@@ -37,8 +37,7 @@ var (
 
 // Schema is the API for creating, migrating and dropping a schema.
 type Schema struct {
-	drv         dialect.Driver
-	universalID bool
+	drv dialect.Driver
 }
 
 // NewSchema creates a new schema client.

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 226 - 130
pkg/database/ent/mutation.go


+ 2 - 2
pkg/database/ent/runtime/runtime.go

@@ -5,6 +5,6 @@ package runtime
 // The schema-stitching logic is generated in github.com/crowdsecurity/crowdsec/pkg/database/ent/runtime.go
 
 const (
-	Version = "v0.9.1"                                          // Version of ent codegen.
-	Sum     = "h1:IG8andyeD79GG24U8Q+1Y45hQXj6gY5evSBcva5gtBk=" // Sum of ent codegen.
+	Version = "v0.10.0"                                         // Version of ent codegen.
+	Sum     = "h1:9cBomE1fh+WX34DPYQL7tDNAIvhKa3tXvwxuLyhYCMo=" // Sum of ent codegen.
 )

+ 4 - 4
pkg/database/ent/tx.go

@@ -40,7 +40,7 @@ type Tx struct {
 }
 
 type (
-	// Committer is the interface that wraps the Committer method.
+	// Committer is the interface that wraps the Commit method.
 	Committer interface {
 		Commit(context.Context, *Tx) error
 	}
@@ -54,7 +54,7 @@ type (
 	// and returns a Committer. For example:
 	//
 	//	hook := func(next ent.Committer) ent.Committer {
-	//		return ent.CommitFunc(func(context.Context, tx *ent.Tx) error {
+	//		return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
 	//			// Do some stuff before.
 	//			if err := next.Commit(ctx, tx); err != nil {
 	//				return err
@@ -95,7 +95,7 @@ func (tx *Tx) OnCommit(f CommitHook) {
 }
 
 type (
-	// Rollbacker is the interface that wraps the Rollbacker method.
+	// Rollbacker is the interface that wraps the Rollback method.
 	Rollbacker interface {
 		Rollback(context.Context, *Tx) error
 	}
@@ -109,7 +109,7 @@ type (
 	// and returns a Rollbacker. For example:
 	//
 	//	hook := func(next ent.Rollbacker) ent.Rollbacker {
-	//		return ent.RollbackFunc(func(context.Context, tx *ent.Tx) error {
+	//		return ent.RollbackFunc(func(ctx context.Context, tx *ent.Tx) error {
 	//			// Do some stuff before.
 	//			if err := next.Rollback(ctx, tx); err != nil {
 	//				return err

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác