diff --git a/cmd/crowdsec-cli/machines.go b/cmd/crowdsec-cli/machines.go index f0d546574..95a58f057 100644 --- a/cmd/crowdsec-cli/machines.go +++ b/cmd/crowdsec-cli/machines.go @@ -134,7 +134,7 @@ Note: This command requires database direct access, so is intended to be run on Run: func(cmd *cobra.Command, args []string) { machines, err := dbClient.ListMachines() if err != nil { - log.Errorf("unable to list blockers: %s", err) + log.Errorf("unable to list machines: %s", err) } if csConfig.Cscli.Output == "human" { table := tablewriter.NewWriter(os.Stdout) @@ -143,7 +143,7 @@ Note: This command requires database direct access, so is intended to be run on table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetHeader([]string{"Name", "IP Address", "Last Update", "Status", "Version"}) + table.SetHeader([]string{"Name", "IP Address", "Last Update", "Status", "Version", "Last Heartbeat"}) for _, w := range machines { var validated string if w.IsValidated { @@ -151,7 +151,12 @@ Note: This command requires database direct access, so is intended to be run on } else { validated = emoji.Prohibited.String() } - table.Append([]string{w.MachineId, w.IpAddress, w.UpdatedAt.Format(time.RFC3339), validated, w.Version}) + lastHeartBeat := time.Now().UTC().Sub(*w.LastHeartbeat) + hbDisplay := lastHeartBeat.Truncate(time.Second).String() + if lastHeartBeat > 2*time.Minute { + hbDisplay = fmt.Sprintf("%s %s", emoji.Warning.String(), lastHeartBeat.Truncate(time.Second).String()) + } + table.Append([]string{w.MachineId, w.IpAddress, w.UpdatedAt.Format(time.RFC3339), validated, w.Version, hbDisplay}) } table.Render() } else if csConfig.Cscli.Output == "json" { @@ -162,7 +167,7 @@ Note: This command requires database direct access, so is intended to be run on fmt.Printf("%s", string(x)) } else if csConfig.Cscli.Output == "raw" { csvwriter := csv.NewWriter(os.Stdout) - err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version"}) + err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "last_heartbeat"}) if err != nil { log.Fatalf("failed to write header: %s", err) } @@ -173,7 +178,7 @@ Note: This command requires database direct access, so is intended to be run on } else { validated = "false" } - err := csvwriter.Write([]string{w.MachineId, w.IpAddress, w.UpdatedAt.Format(time.RFC3339), validated, w.Version}) + err := csvwriter.Write([]string{w.MachineId, w.IpAddress, w.UpdatedAt.Format(time.RFC3339), validated, w.Version, time.Now().UTC().Sub(*w.LastHeartbeat).Truncate(time.Second).String()}) if err != nil { log.Fatalf("failed to write raw output : %s", err) } diff --git a/cmd/crowdsec/output.go b/cmd/crowdsec/output.go index bb7ab1ea1..c47198f0a 100644 --- a/cmd/crowdsec/output.go +++ b/cmd/crowdsec/output.go @@ -100,6 +100,9 @@ func runOutput(input chan types.Event, overflow chan types.Event, buckets *leaky }); err != nil { return errors.Wrapf(err, "authenticate watcher (%s)", apiConfig.Login) } + //start the heartbeat service + log.Debugf("Starting HeartBeat service") + Client.HeartBeat.StartHeartBeat(context.Background(), &outputsTomb) LOOP: for { select { diff --git a/go.sum b/go.sum index 38ce61327..ce01315ad 100644 --- a/go.sum +++ b/go.sum @@ -1035,6 +1035,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7 h1:M1gcVrIb2lSn2FIL19DG0+/b8nNVKJ7W7b4WcAGZAYM= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/apiclient/client.go b/pkg/apiclient/client.go index 82ab9ecfb..8d48993ac 100644 --- a/pkg/apiclient/client.go +++ b/pkg/apiclient/client.go @@ -32,6 +32,7 @@ type ApiClient struct { Auth *AuthService Metrics *MetricsService Signal *SignalService + HeartBeat *HeartBeatService } type service struct { @@ -56,6 +57,7 @@ func NewClient(config *Config) (*ApiClient, error) { c.Auth = (*AuthService)(&c.common) c.Metrics = (*MetricsService)(&c.common) c.Signal = (*SignalService)(&c.common) + c.HeartBeat = (*HeartBeatService)(&c.common) return c, nil } @@ -75,6 +77,8 @@ func NewDefaultClient(URL *url.URL, prefix string, userAgent string, client *htt c.Auth = (*AuthService)(&c.common) c.Metrics = (*MetricsService)(&c.common) c.Signal = (*SignalService)(&c.common) + c.HeartBeat = (*HeartBeatService)(&c.common) + return c, nil } diff --git a/pkg/apiclient/heartbeat.go b/pkg/apiclient/heartbeat.go new file mode 100644 index 000000000..92725ba8b --- /dev/null +++ b/pkg/apiclient/heartbeat.go @@ -0,0 +1,61 @@ +package apiclient + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/crowdsecurity/crowdsec/pkg/types" + log "github.com/sirupsen/logrus" + tomb "gopkg.in/tomb.v2" +) + +type HeartBeatService service + +func (h *HeartBeatService) Ping(ctx context.Context) (bool, *Response, error) { + + u := fmt.Sprintf("%s/heartbeat", h.client.URLPrefix) + + req, err := h.client.NewRequest("GET", u, nil) + if err != nil { + return false, nil, err + } + + resp, err := h.client.Do(ctx, req, nil) + if err != nil { + return false, resp, err + } + + return true, resp, nil +} + +func (h *HeartBeatService) StartHeartBeat(ctx context.Context, t *tomb.Tomb) { + t.Go(func() error { + defer types.CatchPanic("crowdsec/apiClient/heartbeat") + hbTimer := time.NewTicker(1 * time.Minute) + for { + select { + case <-hbTimer.C: + log.Debug("heartbeat: sending heartbeat") + ok, resp, err := h.Ping(ctx) + if err != nil { + log.Errorf("heartbeat error : %s", err) + continue + } + if resp.Response.StatusCode != http.StatusOK { + log.Errorf("heartbeat unexpected return code : %d", resp.Response.StatusCode) + continue + } + if !ok { + log.Errorf("heartbeat returned false") + continue + } + case <-t.Dying(): + log.Debugf("heartbeat: stopping") + hbTimer.Stop() + return nil + } + } + }) +} diff --git a/pkg/apiserver/controllers/controller.go b/pkg/apiserver/controllers/controller.go index 436d9fe33..a93aed6de 100644 --- a/pkg/apiserver/controllers/controller.go +++ b/pkg/apiserver/controllers/controller.go @@ -87,6 +87,7 @@ func (c *Controller) NewV1() error { jwtAuth.DELETE("/alerts", handlerV1.DeleteAlerts) jwtAuth.DELETE("/decisions", handlerV1.DeleteDecisions) jwtAuth.DELETE("/decisions/:decision_id", handlerV1.DeleteDecisionById) + jwtAuth.GET("/heartbeat", handlerV1.HeartBeat) } apiKeyAuth := groupV1.Group("") diff --git a/pkg/apiserver/controllers/v1/heartbeat.go b/pkg/apiserver/controllers/v1/heartbeat.go new file mode 100644 index 000000000..ddd097865 --- /dev/null +++ b/pkg/apiserver/controllers/v1/heartbeat.go @@ -0,0 +1,21 @@ +package v1 + +import ( + "net/http" + + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" +) + +func (c *Controller) HeartBeat(gctx *gin.Context) { + + claims := jwt.ExtractClaims(gctx) + /*TBD : use defines rather than hardcoded key to find back owner*/ + machineID := claims["id"].(string) + + if err := c.DBClient.UpdateMachineLastHeartBeat(machineID); err != nil { + c.HandleDBErrors(gctx, err) + return + } + gctx.Status(http.StatusOK) +} diff --git a/pkg/apiserver/heartbeat_test.go b/pkg/apiserver/heartbeat_test.go new file mode 100644 index 000000000..f09b0d37e --- /dev/null +++ b/pkg/apiserver/heartbeat_test.go @@ -0,0 +1,17 @@ +package apiserver + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHeartBeat(t *testing.T) { + lapi := SetupLAPITest(t) + + w := lapi.RecordResponse("GET", "/v1/heartbeat", emptyBody) + assert.Equal(t, 200, w.Code) + + w = lapi.RecordResponse("POST", "/v1/heartbeat", emptyBody) + assert.Equal(t, 405, w.Code) +} diff --git a/pkg/database/ent/machine.go b/pkg/database/ent/machine.go index 92f707e33..44d198510 100644 --- a/pkg/database/ent/machine.go +++ b/pkg/database/ent/machine.go @@ -22,6 +22,8 @@ type Machine struct { UpdatedAt *time.Time `json:"updated_at,omitempty"` // LastPush holds the value of the "last_push" field. LastPush *time.Time `json:"last_push,omitempty"` + // LastHeartbeat holds the value of the "last_heartbeat" field. + LastHeartbeat *time.Time `json:"last_heartbeat,omitempty"` // MachineId holds the value of the "machineId" field. MachineId string `json:"machineId,omitempty"` // Password holds the value of the "password" field. @@ -70,7 +72,7 @@ func (*Machine) scanValues(columns []string) ([]interface{}, error) { values[i] = new(sql.NullInt64) case machine.FieldMachineId, machine.FieldPassword, machine.FieldIpAddress, machine.FieldScenarios, machine.FieldVersion, machine.FieldStatus: values[i] = new(sql.NullString) - case machine.FieldCreatedAt, machine.FieldUpdatedAt, machine.FieldLastPush: + case machine.FieldCreatedAt, machine.FieldUpdatedAt, machine.FieldLastPush, machine.FieldLastHeartbeat: values[i] = new(sql.NullTime) default: return nil, fmt.Errorf("unexpected column %q for type Machine", columns[i]) @@ -114,6 +116,13 @@ func (m *Machine) assignValues(columns []string, values []interface{}) error { m.LastPush = new(time.Time) *m.LastPush = value.Time } + case machine.FieldLastHeartbeat: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_heartbeat", values[i]) + } else if value.Valid { + m.LastHeartbeat = new(time.Time) + *m.LastHeartbeat = value.Time + } case machine.FieldMachineId: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field machineId", values[i]) @@ -201,6 +210,10 @@ func (m *Machine) String() string { builder.WriteString(", last_push=") builder.WriteString(v.Format(time.ANSIC)) } + if v := m.LastHeartbeat; v != nil { + builder.WriteString(", last_heartbeat=") + builder.WriteString(v.Format(time.ANSIC)) + } builder.WriteString(", machineId=") builder.WriteString(m.MachineId) builder.WriteString(", password=") diff --git a/pkg/database/ent/machine/machine.go b/pkg/database/ent/machine/machine.go index 36a1f21c4..b8e6c71fe 100644 --- a/pkg/database/ent/machine/machine.go +++ b/pkg/database/ent/machine/machine.go @@ -17,6 +17,8 @@ const ( FieldUpdatedAt = "updated_at" // FieldLastPush holds the string denoting the last_push field in the database. FieldLastPush = "last_push" + // FieldLastHeartbeat holds the string denoting the last_heartbeat field in the database. + FieldLastHeartbeat = "last_heartbeat" // FieldMachineId holds the string denoting the machineid field in the database. FieldMachineId = "machine_id" // FieldPassword holds the string denoting the password field in the database. @@ -50,6 +52,7 @@ var Columns = []string{ FieldCreatedAt, FieldUpdatedAt, FieldLastPush, + FieldLastHeartbeat, FieldMachineId, FieldPassword, FieldIpAddress, @@ -82,6 +85,10 @@ var ( DefaultLastPush func() time.Time // UpdateDefaultLastPush holds the default value on update for the "last_push" field. UpdateDefaultLastPush func() time.Time + // DefaultLastHeartbeat holds the default value on creation for the "last_heartbeat" field. + DefaultLastHeartbeat func() time.Time + // UpdateDefaultLastHeartbeat holds the default value on update for the "last_heartbeat" field. + UpdateDefaultLastHeartbeat func() time.Time // ScenariosValidator is a validator for the "scenarios" field. It is called by the builders before save. ScenariosValidator func(string) error // DefaultIsValidated holds the default value on creation for the "isValidated" field. diff --git a/pkg/database/ent/machine/where.go b/pkg/database/ent/machine/where.go index 2b46ba9a1..361405e8e 100644 --- a/pkg/database/ent/machine/where.go +++ b/pkg/database/ent/machine/where.go @@ -114,6 +114,13 @@ func LastPush(v time.Time) predicate.Machine { }) } +// LastHeartbeat applies equality check predicate on the "last_heartbeat" field. It's identical to LastHeartbeatEQ. +func LastHeartbeat(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldLastHeartbeat), v)) + }) +} + // MachineId applies equality check predicate on the "machineId" field. It's identical to MachineIdEQ. func MachineId(v string) predicate.Machine { return predicate.Machine(func(s *sql.Selector) { @@ -433,6 +440,96 @@ func LastPushNotNil() predicate.Machine { }) } +// LastHeartbeatEQ applies the EQ predicate on the "last_heartbeat" field. +func LastHeartbeatEQ(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldLastHeartbeat), v)) + }) +} + +// LastHeartbeatNEQ applies the NEQ predicate on the "last_heartbeat" field. +func LastHeartbeatNEQ(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldLastHeartbeat), v)) + }) +} + +// LastHeartbeatIn applies the In predicate on the "last_heartbeat" field. +func LastHeartbeatIn(vs ...time.Time) predicate.Machine { + v := make([]interface{}, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Machine(func(s *sql.Selector) { + // if not arguments were provided, append the FALSE constants, + // since we can't apply "IN ()". This will make this predicate falsy. + if len(v) == 0 { + s.Where(sql.False()) + return + } + s.Where(sql.In(s.C(FieldLastHeartbeat), v...)) + }) +} + +// LastHeartbeatNotIn applies the NotIn predicate on the "last_heartbeat" field. +func LastHeartbeatNotIn(vs ...time.Time) predicate.Machine { + v := make([]interface{}, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Machine(func(s *sql.Selector) { + // if not arguments were provided, append the FALSE constants, + // since we can't apply "IN ()". This will make this predicate falsy. + if len(v) == 0 { + s.Where(sql.False()) + return + } + s.Where(sql.NotIn(s.C(FieldLastHeartbeat), v...)) + }) +} + +// LastHeartbeatGT applies the GT predicate on the "last_heartbeat" field. +func LastHeartbeatGT(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.GT(s.C(FieldLastHeartbeat), v)) + }) +} + +// LastHeartbeatGTE applies the GTE predicate on the "last_heartbeat" field. +func LastHeartbeatGTE(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.GTE(s.C(FieldLastHeartbeat), v)) + }) +} + +// LastHeartbeatLT applies the LT predicate on the "last_heartbeat" field. +func LastHeartbeatLT(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.LT(s.C(FieldLastHeartbeat), v)) + }) +} + +// LastHeartbeatLTE applies the LTE predicate on the "last_heartbeat" field. +func LastHeartbeatLTE(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.LTE(s.C(FieldLastHeartbeat), v)) + }) +} + +// LastHeartbeatIsNil applies the IsNil predicate on the "last_heartbeat" field. +func LastHeartbeatIsNil() predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.IsNull(s.C(FieldLastHeartbeat))) + }) +} + +// LastHeartbeatNotNil applies the NotNil predicate on the "last_heartbeat" field. +func LastHeartbeatNotNil() predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.NotNull(s.C(FieldLastHeartbeat))) + }) +} + // MachineIdEQ applies the EQ predicate on the "machineId" field. func MachineIdEQ(v string) predicate.Machine { return predicate.Machine(func(s *sql.Selector) { diff --git a/pkg/database/ent/machine_create.go b/pkg/database/ent/machine_create.go index 21f26090a..2d76a3fc9 100644 --- a/pkg/database/ent/machine_create.go +++ b/pkg/database/ent/machine_create.go @@ -63,6 +63,20 @@ func (mc *MachineCreate) SetNillableLastPush(t *time.Time) *MachineCreate { return mc } +// SetLastHeartbeat sets the "last_heartbeat" field. +func (mc *MachineCreate) SetLastHeartbeat(t time.Time) *MachineCreate { + mc.mutation.SetLastHeartbeat(t) + return mc +} + +// SetNillableLastHeartbeat sets the "last_heartbeat" field if the given value is not nil. +func (mc *MachineCreate) SetNillableLastHeartbeat(t *time.Time) *MachineCreate { + if t != nil { + mc.SetLastHeartbeat(*t) + } + return mc +} + // SetMachineId sets the "machineId" field. func (mc *MachineCreate) SetMachineId(s string) *MachineCreate { mc.mutation.SetMachineId(s) @@ -235,6 +249,10 @@ func (mc *MachineCreate) defaults() { v := machine.DefaultLastPush() mc.mutation.SetLastPush(v) } + if _, ok := mc.mutation.LastHeartbeat(); !ok { + v := machine.DefaultLastHeartbeat() + mc.mutation.SetLastHeartbeat(v) + } if _, ok := mc.mutation.IsValidated(); !ok { v := machine.DefaultIsValidated mc.mutation.SetIsValidated(v) @@ -311,6 +329,14 @@ func (mc *MachineCreate) createSpec() (*Machine, *sqlgraph.CreateSpec) { }) _node.LastPush = &value } + if value, ok := mc.mutation.LastHeartbeat(); ok { + _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: machine.FieldLastHeartbeat, + }) + _node.LastHeartbeat = &value + } if value, ok := mc.mutation.MachineId(); ok { _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ Type: field.TypeString, diff --git a/pkg/database/ent/machine_update.go b/pkg/database/ent/machine_update.go index a1a8f4b6e..b396ef1d4 100644 --- a/pkg/database/ent/machine_update.go +++ b/pkg/database/ent/machine_update.go @@ -65,6 +65,18 @@ func (mu *MachineUpdate) ClearLastPush() *MachineUpdate { return mu } +// SetLastHeartbeat sets the "last_heartbeat" field. +func (mu *MachineUpdate) SetLastHeartbeat(t time.Time) *MachineUpdate { + mu.mutation.SetLastHeartbeat(t) + return mu +} + +// ClearLastHeartbeat clears the value of the "last_heartbeat" field. +func (mu *MachineUpdate) ClearLastHeartbeat() *MachineUpdate { + mu.mutation.ClearLastHeartbeat() + return mu +} + // SetMachineId sets the "machineId" field. func (mu *MachineUpdate) SetMachineId(s string) *MachineUpdate { mu.mutation.SetMachineId(s) @@ -273,6 +285,10 @@ func (mu *MachineUpdate) defaults() { v := machine.UpdateDefaultLastPush() mu.mutation.SetLastPush(v) } + if _, ok := mu.mutation.LastHeartbeat(); !ok && !mu.mutation.LastHeartbeatCleared() { + v := machine.UpdateDefaultLastHeartbeat() + mu.mutation.SetLastHeartbeat(v) + } } // check runs all checks and user-defined validators on the builder. @@ -342,6 +358,19 @@ func (mu *MachineUpdate) sqlSave(ctx context.Context) (n int, err error) { Column: machine.FieldLastPush, }) } + if value, ok := mu.mutation.LastHeartbeat(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: machine.FieldLastHeartbeat, + }) + } + if mu.mutation.LastHeartbeatCleared() { + _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Column: machine.FieldLastHeartbeat, + }) + } if value, ok := mu.mutation.MachineId(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeString, @@ -518,6 +547,18 @@ func (muo *MachineUpdateOne) ClearLastPush() *MachineUpdateOne { return muo } +// SetLastHeartbeat sets the "last_heartbeat" field. +func (muo *MachineUpdateOne) SetLastHeartbeat(t time.Time) *MachineUpdateOne { + muo.mutation.SetLastHeartbeat(t) + return muo +} + +// ClearLastHeartbeat clears the value of the "last_heartbeat" field. +func (muo *MachineUpdateOne) ClearLastHeartbeat() *MachineUpdateOne { + muo.mutation.ClearLastHeartbeat() + return muo +} + // SetMachineId sets the "machineId" field. func (muo *MachineUpdateOne) SetMachineId(s string) *MachineUpdateOne { muo.mutation.SetMachineId(s) @@ -733,6 +774,10 @@ func (muo *MachineUpdateOne) defaults() { v := machine.UpdateDefaultLastPush() muo.mutation.SetLastPush(v) } + if _, ok := muo.mutation.LastHeartbeat(); !ok && !muo.mutation.LastHeartbeatCleared() { + v := machine.UpdateDefaultLastHeartbeat() + muo.mutation.SetLastHeartbeat(v) + } } // check runs all checks and user-defined validators on the builder. @@ -819,6 +864,19 @@ func (muo *MachineUpdateOne) sqlSave(ctx context.Context) (_node *Machine, err e Column: machine.FieldLastPush, }) } + if value, ok := muo.mutation.LastHeartbeat(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: machine.FieldLastHeartbeat, + }) + } + if muo.mutation.LastHeartbeatCleared() { + _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Column: machine.FieldLastHeartbeat, + }) + } if value, ok := muo.mutation.MachineId(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeString, diff --git a/pkg/database/ent/migrate/schema.go b/pkg/database/ent/migrate/schema.go index 81446b409..891fdee89 100644 --- a/pkg/database/ent/migrate/schema.go +++ b/pkg/database/ent/migrate/schema.go @@ -155,6 +155,7 @@ var ( {Name: "created_at", Type: field.TypeTime, Nullable: true}, {Name: "updated_at", Type: field.TypeTime, Nullable: true}, {Name: "last_push", Type: field.TypeTime, Nullable: true}, + {Name: "last_heartbeat", Type: field.TypeTime, Nullable: true}, {Name: "machine_id", Type: field.TypeString, Unique: true}, {Name: "password", Type: field.TypeString}, {Name: "ip_address", Type: field.TypeString}, diff --git a/pkg/database/ent/mutation.go b/pkg/database/ent/mutation.go index 29e17d942..7427e6b28 100644 --- a/pkg/database/ent/mutation.go +++ b/pkg/database/ent/mutation.go @@ -5232,26 +5232,27 @@ func (m *EventMutation) ResetEdge(name string) error { // MachineMutation represents an operation that mutates the Machine nodes in the graph. type MachineMutation struct { config - op Op - typ string - id *int - created_at *time.Time - updated_at *time.Time - last_push *time.Time - machineId *string - password *string - ipAddress *string - scenarios *string - version *string - isValidated *bool - status *string - clearedFields map[string]struct{} - alerts map[int]struct{} - removedalerts map[int]struct{} - clearedalerts bool - done bool - oldValue func(context.Context) (*Machine, error) - predicates []predicate.Machine + op Op + typ string + id *int + created_at *time.Time + updated_at *time.Time + last_push *time.Time + last_heartbeat *time.Time + machineId *string + password *string + ipAddress *string + scenarios *string + version *string + isValidated *bool + status *string + clearedFields map[string]struct{} + alerts map[int]struct{} + removedalerts map[int]struct{} + clearedalerts bool + done bool + oldValue func(context.Context) (*Machine, error) + predicates []predicate.Machine } var _ ent.Mutation = (*MachineMutation)(nil) @@ -5499,6 +5500,55 @@ func (m *MachineMutation) ResetLastPush() { delete(m.clearedFields, machine.FieldLastPush) } +// SetLastHeartbeat sets the "last_heartbeat" field. +func (m *MachineMutation) SetLastHeartbeat(t time.Time) { + m.last_heartbeat = &t +} + +// LastHeartbeat returns the value of the "last_heartbeat" field in the mutation. +func (m *MachineMutation) LastHeartbeat() (r time.Time, exists bool) { + v := m.last_heartbeat + if v == nil { + return + } + return *v, true +} + +// OldLastHeartbeat returns the old "last_heartbeat" field's value of the Machine entity. +// If the Machine object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MachineMutation) OldLastHeartbeat(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldLastHeartbeat is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldLastHeartbeat requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastHeartbeat: %w", err) + } + return oldValue.LastHeartbeat, nil +} + +// ClearLastHeartbeat clears the value of the "last_heartbeat" field. +func (m *MachineMutation) ClearLastHeartbeat() { + m.last_heartbeat = nil + m.clearedFields[machine.FieldLastHeartbeat] = struct{}{} +} + +// LastHeartbeatCleared returns if the "last_heartbeat" field was cleared in this mutation. +func (m *MachineMutation) LastHeartbeatCleared() bool { + _, ok := m.clearedFields[machine.FieldLastHeartbeat] + return ok +} + +// ResetLastHeartbeat resets all changes to the "last_heartbeat" field. +func (m *MachineMutation) ResetLastHeartbeat() { + m.last_heartbeat = nil + delete(m.clearedFields, machine.FieldLastHeartbeat) +} + // SetMachineId sets the "machineId" field. func (m *MachineMutation) SetMachineId(s string) { m.machineId = &s @@ -5863,7 +5913,7 @@ func (m *MachineMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *MachineMutation) Fields() []string { - fields := make([]string, 0, 10) + fields := make([]string, 0, 11) if m.created_at != nil { fields = append(fields, machine.FieldCreatedAt) } @@ -5873,6 +5923,9 @@ func (m *MachineMutation) Fields() []string { if m.last_push != nil { fields = append(fields, machine.FieldLastPush) } + if m.last_heartbeat != nil { + fields = append(fields, machine.FieldLastHeartbeat) + } if m.machineId != nil { fields = append(fields, machine.FieldMachineId) } @@ -5908,6 +5961,8 @@ func (m *MachineMutation) Field(name string) (ent.Value, bool) { return m.UpdatedAt() case machine.FieldLastPush: return m.LastPush() + case machine.FieldLastHeartbeat: + return m.LastHeartbeat() case machine.FieldMachineId: return m.MachineId() case machine.FieldPassword: @@ -5937,6 +5992,8 @@ func (m *MachineMutation) OldField(ctx context.Context, name string) (ent.Value, return m.OldUpdatedAt(ctx) case machine.FieldLastPush: return m.OldLastPush(ctx) + case machine.FieldLastHeartbeat: + return m.OldLastHeartbeat(ctx) case machine.FieldMachineId: return m.OldMachineId(ctx) case machine.FieldPassword: @@ -5981,6 +6038,13 @@ func (m *MachineMutation) SetField(name string, value ent.Value) error { } m.SetLastPush(v) return nil + case machine.FieldLastHeartbeat: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastHeartbeat(v) + return nil case machine.FieldMachineId: v, ok := value.(string) if !ok { @@ -6069,6 +6133,9 @@ func (m *MachineMutation) ClearedFields() []string { if m.FieldCleared(machine.FieldLastPush) { fields = append(fields, machine.FieldLastPush) } + if m.FieldCleared(machine.FieldLastHeartbeat) { + fields = append(fields, machine.FieldLastHeartbeat) + } if m.FieldCleared(machine.FieldScenarios) { fields = append(fields, machine.FieldScenarios) } @@ -6101,6 +6168,9 @@ func (m *MachineMutation) ClearField(name string) error { case machine.FieldLastPush: m.ClearLastPush() return nil + case machine.FieldLastHeartbeat: + m.ClearLastHeartbeat() + return nil case machine.FieldScenarios: m.ClearScenarios() return nil @@ -6127,6 +6197,9 @@ func (m *MachineMutation) ResetField(name string) error { case machine.FieldLastPush: m.ResetLastPush() return nil + case machine.FieldLastHeartbeat: + m.ResetLastHeartbeat() + return nil case machine.FieldMachineId: m.ResetMachineId() return nil diff --git a/pkg/database/ent/runtime.go b/pkg/database/ent/runtime.go index f6e42dd41..c12c025ce 100644 --- a/pkg/database/ent/runtime.go +++ b/pkg/database/ent/runtime.go @@ -138,12 +138,18 @@ func init() { machine.DefaultLastPush = machineDescLastPush.Default.(func() time.Time) // machine.UpdateDefaultLastPush holds the default value on update for the last_push field. machine.UpdateDefaultLastPush = machineDescLastPush.UpdateDefault.(func() time.Time) + // machineDescLastHeartbeat is the schema descriptor for last_heartbeat field. + machineDescLastHeartbeat := machineFields[3].Descriptor() + // machine.DefaultLastHeartbeat holds the default value on creation for the last_heartbeat field. + machine.DefaultLastHeartbeat = machineDescLastHeartbeat.Default.(func() time.Time) + // machine.UpdateDefaultLastHeartbeat holds the default value on update for the last_heartbeat field. + machine.UpdateDefaultLastHeartbeat = machineDescLastHeartbeat.UpdateDefault.(func() time.Time) // machineDescScenarios is the schema descriptor for scenarios field. - machineDescScenarios := machineFields[6].Descriptor() + machineDescScenarios := machineFields[7].Descriptor() // machine.ScenariosValidator is a validator for the "scenarios" field. It is called by the builders before save. machine.ScenariosValidator = machineDescScenarios.Validators[0].(func(string) error) // machineDescIsValidated is the schema descriptor for isValidated field. - machineDescIsValidated := machineFields[8].Descriptor() + machineDescIsValidated := machineFields[9].Descriptor() // machine.DefaultIsValidated holds the default value on creation for the isValidated field. machine.DefaultIsValidated = machineDescIsValidated.Default.(bool) metaFields := schema.Meta{}.Fields() diff --git a/pkg/database/ent/schema/machine.go b/pkg/database/ent/schema/machine.go index 7a605fc85..4512bdf4a 100644 --- a/pkg/database/ent/schema/machine.go +++ b/pkg/database/ent/schema/machine.go @@ -24,6 +24,9 @@ func (Machine) Fields() []ent.Field { field.Time("last_push"). Default(types.UtcNow). UpdateDefault(types.UtcNow).Nillable().Optional(), + field.Time("last_heartbeat"). + Default(types.UtcNow). + UpdateDefault(types.UtcNow).Nillable().Optional(), field.String("machineId").Unique(), field.String("password").Sensitive(), field.String("ipAddress"), diff --git a/pkg/database/machines.go b/pkg/database/machines.go index 5a828bc14..ab4505cba 100644 --- a/pkg/database/machines.go +++ b/pkg/database/machines.go @@ -118,6 +118,14 @@ func (c *Client) UpdateMachineLastPush(machineID string) error { return nil } +func (c *Client) UpdateMachineLastHeartBeat(machineID string) error { + _, err := c.Ent.Machine.Update().Where(machine.MachineIdEQ(machineID)).SetLastHeartbeat(time.Now().UTC()).Save(c.CTX) + if err != nil { + return errors.Wrapf(UpdateFail, "updating machine last_heartbeat: %s", err) + } + return nil +} + func (c *Client) UpdateMachineScenarios(scenarios string, ID int) error { _, err := c.Ent.Machine.UpdateOneID(ID). SetUpdatedAt(time.Now().UTC()).