Compare commits

...

2 commits

Author SHA1 Message Date
414097108f
wip 2024-05-23 20:34:52 +02:00
0f28cdf175
feat: refactor, renames almost working workflow 2024-05-23 18:33:30 +02:00
82 changed files with 1865 additions and 971 deletions

View file

@ -2,8 +2,6 @@
Golang selfhosted Status/Healthcheck monitoring app.
Mostly just a project to test [temporal.io](https://temporal.io/).
### Roadmap
- [x] SSO Support for authentication.
- [x] SQLite for database.
@ -37,9 +35,12 @@ Demo is available at https://zdravko.mnts.dev.
```sh
# Configure
# You will need to configure an SSO provider
# This can be github for example.
cp example.env .env
# Generate JWT key
# Copy the values to your .env
just generate-jwt-key
# Start development environment

View file

@ -8,10 +8,10 @@ import (
"sync"
"syscall"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/pkg/server"
"code.tjo.space/mentos1386/zdravko/pkg/temporal"
"code.tjo.space/mentos1386/zdravko/pkg/worker"
"github.com/mentos1386/zdravko/internal/config"
"github.com/mentos1386/zdravko/pkg/server"
"github.com/mentos1386/zdravko/pkg/temporal"
"github.com/mentos1386/zdravko/pkg/worker"
)
type StartableAndStoppable interface {

View file

@ -1,4 +1,4 @@
package kv
package database
import (
"time"

View file

@ -1,4 +1,4 @@
package kv
package database
import "time"

View file

@ -45,15 +45,6 @@ type OAuth2State struct {
ExpiresAt *Time `db:"expires_at"`
}
type CheckStatus string
const (
CheckStatusSuccess CheckStatus = "SUCCESS"
CheckStatusFailure CheckStatus = "FAILURE"
CheckStatusError CheckStatus = "ERROR"
CheckStatusUnknown CheckStatus = "UNKNOWN"
)
type CheckState string
const (
@ -62,45 +53,24 @@ const (
CheckStateUnknown CheckState = "UNKNOWN"
)
type CheckVisibility string
const (
CheckVisibilityPublic CheckVisibility = "PUBLIC"
CheckVisibilityPrivate CheckVisibility = "PRIVATE"
CheckVisibilityUnknown CheckVisibility = "UNKNOWN"
)
type Check struct {
CreatedAt *Time `db:"created_at"`
UpdatedAt *Time `db:"updated_at"`
Id string `db:"id"`
Name string `db:"name"`
Group string `db:"group"`
Visibility CheckVisibility `db:"visibility"`
Id string `db:"id"`
Name string `db:"name"`
Schedule string `db:"schedule"`
Script string `db:"script"`
Filter string `db:"filter"`
}
type CheckWithWorkerGroups struct {
Check
// List of worker group names
WorkerGroups []string
}
type CheckHistory struct {
CreatedAt *Time `db:"created_at"`
CheckId string `db:"check_id"`
Status CheckStatus `db:"status"`
Note string `db:"note"`
WorkerGroupId string `db:"worker_group_id"`
WorkerGroupName string `db:"worker_group_name"`
}
type WorkerGroup struct {
CreatedAt *Time `db:"created_at"`
UpdatedAt *Time `db:"updated_at"`
@ -116,15 +86,6 @@ type WorkerGroupWithChecks struct {
Checks []string
}
type TriggerStatus string
const (
TriggerStatusSuccess TriggerStatus = "SUCCESS"
TriggerStatusFailure TriggerStatus = "FAILURE"
TriggerStatusError TriggerStatus = "ERROR"
TriggerStatusUnknown TriggerStatus = "UNKNOWN"
)
type TriggerState string
const (
@ -133,14 +94,6 @@ const (
TriggerStateUnknown TriggerState = "UNKNOWN"
)
type TriggerVisibility string
const (
TriggerVisibilityPublic TriggerVisibility = "PUBLIC"
TriggerVisibilityPrivate TriggerVisibility = "PRIVATE"
TriggerVisibilityUnknown TriggerVisibility = "UNKNOWN"
)
type Trigger struct {
CreatedAt *Time `db:"created_at"`
UpdatedAt *Time `db:"updated_at"`
@ -150,10 +103,46 @@ type Trigger struct {
Script string `db:"script"`
}
type TriggerHistory struct {
type TargetVisibility string
const (
TargetVisibilityPublic TargetVisibility = "PUBLIC"
TargetVisibilityPrivate TargetVisibility = "PRIVATE"
TargetVisibilityUnknown TargetVisibility = "UNKNOWN"
)
type TargetState string
const (
TargetStateActive TargetState = "ACTIVE"
TargetStatePaused TargetState = "PAUSED"
TargetStateUnknown TargetState = "UNKNOWN"
)
type Target struct {
CreatedAt *Time `db:"created_at"`
UpdatedAt *Time `db:"updated_at"`
Id string `db:"id"`
Name string `db:"name"`
Group string `db:"group"`
Visibility TargetVisibility `db:"visibility"`
State TargetState `db:"state"`
Metadata string `db:"metadata"`
}
type TargetStatus string
const (
TargetStatusSuccess TargetStatus = "SUCCESS"
TargetStatusFailure TargetStatus = "FAILURE"
TargetStatusUnknown TargetStatus = "UNKNOWN"
)
type TargetHistory struct {
CreatedAt *Time `db:"created_at"`
TriggerId string `db:"trigger_id"`
Status TriggerStatus `db:"status"`
Note string `db:"note"`
TargetId string `db:"target_id"`
Status TargetStatus `db:"status"`
Note string `db:"note"`
}

View file

@ -9,11 +9,10 @@ CREATE TABLE oauth2_states (
CREATE TABLE checks (
id TEXT NOT NULL,
name TEXT NOT NULL,
"group" TEXT NOT NULL,
schedule TEXT NOT NULL,
script TEXT NOT NULL,
visibility TEXT NOT NULL,
filter TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
@ -21,8 +20,6 @@ CREATE TABLE checks (
PRIMARY KEY (id),
CONSTRAINT unique_checks_name UNIQUE (name)
) STRICT;
-- +migrate StatementBegin
CREATE TRIGGER checks_updated_timestamp AFTER UPDATE ON checks BEGIN
UPDATE checks SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = NEW.id;
@ -39,7 +36,6 @@ CREATE TABLE worker_groups (
PRIMARY KEY (id),
CONSTRAINT unique_worker_groups_name UNIQUE (name)
) STRICT;
-- +migrate StatementBegin
CREATE TRIGGER worker_groups_updated_timestamp AFTER UPDATE ON worker_groups BEGIN
UPDATE worker_groups SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = NEW.id;
@ -55,19 +51,6 @@ CREATE TABLE check_worker_groups (
CONSTRAINT fk_check_worker_groups_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE
) STRICT;
CREATE TABLE check_histories (
check_id TEXT NOT NULL,
worker_group_id TEXT NOT NULL,
status TEXT NOT NULL,
note TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (check_id, worker_group_id, created_at),
CONSTRAINT fk_check_histories_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE,
CONSTRAINT fk_check_histories_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE
) STRICT;
CREATE TABLE triggers (
id TEXT NOT NULL,
@ -80,24 +63,44 @@ CREATE TABLE triggers (
PRIMARY KEY (id),
CONSTRAINT unique_triggers_name UNIQUE (name)
) STRICT;
-- +migrate StatementBegin
CREATE TRIGGER triggers_updated_timestamp AFTER UPDATE ON triggers BEGIN
UPDATE triggers SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = NEW.id;
END;
-- +migrate StatementEnd
CREATE TABLE trigger_histories (
trigger_id TEXT NOT NULL,
CREATE TABLE targets (
id TEXT NOT NULL,
name TEXT NOT NULL,
"group" TEXT NOT NULL,
visibility TEXT NOT NULL,
state TEXT NOT NULL,
metadata TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (id),
CONSTRAINT unique_targets_name UNIQUE (name)
) STRICT;
-- +migrate StatementBegin
CREATE TRIGGER targets_updated_timestamp AFTER UPDATE ON targets BEGIN
UPDATE targets SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = NEW.id;
END;
-- +migrate StatementEnd
CREATE TABLE target_histories (
target_id TEXT NOT NULL,
status TEXT NOT NULL,
note TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (trigger_id, created_at),
CONSTRAINT fk_trigger_histories_trigger FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE
PRIMARY KEY (target_id, created_at),
CONSTRAINT fk_target_histories_target FOREIGN KEY (target_id) REFERENCES targets(id) ON DELETE CASCADE
) STRICT;
-- +migrate Down
@ -105,9 +108,7 @@ DROP TABLE oauth2_states;
DROP TABLE check_worker_groups;
DROP TABLE worker_groups;
DROP TRIGGER worker_groups_updated_timestamp;
DROP TABLE check_histories;
DROP TABLE checks;
DROP TRIGGER checks_updated_timestamp;
DROP TABLE triggers;
DROP TABLE trigger_histories;
DROP TRIGGER triggers_updated_timestamp;

View file

@ -7,8 +7,11 @@
SESSION_SECRET=your_secret
# To generate keys, run "just generate-jwt-key"
JWT_PUBLIC_KEY=""
JWT_PRIVATE_KEY=""
# When running `just run` or `just run-worker`
# This doesn't have to be set, as it's read from the file
#
#JWT_PUBLIC_KEY=""
#JWT_PRIVATE_KEY=""
# To generate worker token, go to website and
# create new worker. Then copy the token.

19
go.mod
View file

@ -1,4 +1,4 @@
module code.tjo.space/mentos1386/zdravko
module github.com/mentos1386/zdravko
go 1.21.6
@ -20,7 +20,7 @@ require (
go.temporal.io/api v1.27.0
go.temporal.io/sdk v1.26.0-rc.2
go.temporal.io/server v1.22.4
golang.org/x/exp v0.0.0-20231127185646-65229373498e
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/oauth2 v0.17.0
gopkg.in/yaml.v2 v2.4.0
)
@ -98,6 +98,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/mql v0.1.4 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
@ -177,14 +178,14 @@ require (
go.uber.org/fx v1.20.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.17.0 // indirect
golang.org/x/tools v0.21.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/api v0.155.0 // indirect
google.golang.org/appengine v1.6.8 // indirect

11
go.sum
View file

@ -265,6 +265,8 @@ github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/mql v0.1.4 h1:6ivb4DDJk1/OcEbuHM+976SJlCOsC8F9Dqg2UTBRtIM=
github.com/hashicorp/mql v0.1.4/go.mod h1:95bDX8gpwGlRRN/IG8FCTwxH6Uy5PV2x+8FrdBxYqF0=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
@ -571,6 +573,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -579,6 +582,8 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -596,6 +601,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -620,6 +626,7 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -637,6 +644,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -670,6 +678,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -686,6 +695,7 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -708,6 +718,7 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -1,11 +0,0 @@
package activities
import "code.tjo.space/mentos1386/zdravko/internal/config"
type Activities struct {
config *config.WorkerConfig
}
func NewActivities(config *config.WorkerConfig) *Activities {
return &Activities{config: config}
}

View file

@ -1,77 +0,0 @@
package activities
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/script"
"code.tjo.space/mentos1386/zdravko/pkg/api"
"code.tjo.space/mentos1386/zdravko/pkg/k6"
)
type HealtcheckParam struct {
Script string
}
type CheckResult struct {
Success bool
Note string
}
func (a *Activities) Check(ctx context.Context, param HealtcheckParam) (*CheckResult, error) {
execution := k6.NewExecution(slog.Default(), script.UnescapeString(param.Script))
result, err := execution.Run(ctx)
if err != nil {
return nil, err
}
return &CheckResult{Success: result.Success, Note: result.Note}, nil
}
type HealtcheckAddToHistoryParam struct {
CheckId string
Status models.CheckStatus
Note string
WorkerGroupId string
}
type CheckAddToHistoryResult struct {
}
func (a *Activities) CheckAddToHistory(ctx context.Context, param HealtcheckAddToHistoryParam) (*CheckAddToHistoryResult, error) {
url := fmt.Sprintf("%s/api/v1/checks/%s/history", a.config.ApiUrl, param.CheckId)
body := api.ApiV1ChecksHistoryPOSTBody{
Status: param.Status,
Note: param.Note,
WorkerGroupId: param.WorkerGroupId,
}
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := api.NewRequest(http.MethodPost, url, a.config.Token, bytes.NewReader(jsonBody))
if err != nil {
return nil, err
}
response, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode)
}
return &CheckAddToHistoryResult{}, nil
}

View file

@ -1,72 +0,0 @@
package handlers
import (
"context"
"database/sql"
"errors"
"net/http"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/services"
"code.tjo.space/mentos1386/zdravko/pkg/api"
"github.com/labstack/echo/v4"
)
type ApiV1WorkersConnectGETResponse struct {
Endpoint string `json:"endpoint"`
Group string `json:"group"`
}
func (h *BaseHandler) ApiV1WorkersConnectGET(c echo.Context) error {
ctx := context.Background()
cc := c.(AuthenticatedContext)
workerGroup, err := services.GetWorkerGroup(ctx, h.db, cc.Principal.Worker.Group)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return echo.NewHTTPError(http.StatusUnauthorized, "Token invalid")
}
return err
}
response := ApiV1WorkersConnectGETResponse{
Endpoint: h.config.Temporal.ServerHost,
Group: workerGroup.Id,
}
return c.JSON(http.StatusOK, response)
}
// TODO: Can we instead get this from the Workflow outcome?
//
// To somehow listen for the outcomes and then store them automatically.
func (h *BaseHandler) ApiV1ChecksHistoryPOST(c echo.Context) error {
ctx := context.Background()
id := c.Param("id")
var body api.ApiV1ChecksHistoryPOSTBody
err := (&echo.DefaultBinder{}).BindBody(c, &body)
if err != nil {
return err
}
_, err = services.GetCheck(ctx, h.db, id)
if err != nil {
if err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "Check not found")
}
return err
}
err = services.AddHistoryForCheck(ctx, h.db, &models.CheckHistory{
CheckId: id,
WorkerGroupId: body.WorkerGroupId,
Status: body.Status,
Note: body.Note,
})
if err != nil {
return err
}
return c.JSON(http.StatusCreated, map[string]string{"status": "ok"})
}

View file

@ -1,30 +0,0 @@
package handlers
import (
"net/http"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4"
)
type Target struct{}
type SettingsTargets struct {
*Settings
Targets []*Target
}
func (h *BaseHandler) SettingsTargetsGET(c echo.Context) error {
cc := c.(AuthenticatedContext)
targets := make([]*Target, 0)
return c.Render(http.StatusOK, "settings_targets.tmpl", &SettingsTargets{
Settings: NewSettings(
cc.Principal.User,
GetPageByTitle(SettingsPages, "Targets"),
[]*components.Page{GetPageByTitle(SettingsPages, "Targets")},
),
Targets: targets,
})
}

View file

@ -0,0 +1,20 @@
package activities
import (
"log/slog"
"github.com/jmoiron/sqlx"
"github.com/mentos1386/zdravko/database"
"github.com/mentos1386/zdravko/internal/config"
)
type Activities struct {
config *config.ServerConfig
db *sqlx.DB
kvStore database.KeyValueStore
logger *slog.Logger
}
func NewActivities(config *config.ServerConfig, logger *slog.Logger, db *sqlx.DB, kvStore database.KeyValueStore) *Activities {
return &Activities{config: config, logger: logger, db: db, kvStore: kvStore}
}

View file

@ -0,0 +1,11 @@
package activities
import (
"context"
"github.com/mentos1386/zdravko/internal/temporal"
)
func (a *Activities) ProcessCheckOutcome(ctx context.Context, param temporal.ActivityProcessCheckOutcomeParam) (*temporal.ActivityProcessCheckOutcomeResult, error) {
return nil, nil
}

View file

@ -0,0 +1,28 @@
package activities
import (
"context"
"github.com/hashicorp/mql"
"github.com/mentos1386/zdravko/internal/server/services"
"github.com/mentos1386/zdravko/internal/temporal"
)
func (a *Activities) TargetsFilter(ctx context.Context, param temporal.ActivityTargetsFilterParam) (*temporal.ActivityTargetsFilterResult, error) {
a.logger.Info("TargetsFilter", "filter", param.Filter)
f, err := mql.Parse(param.Filter)
if err != nil {
return nil, err
}
a.logger.Info("TargetsFilter", "filter", f)
// TODO: Parse filter.
targets, err := services.GetTargetsWithFilter(ctx, a.db, param.Filter)
if err != nil {
return nil, err
}
return &temporal.ActivityTargetsFilterResult{
Targets: targets,
}, nil
}

View file

@ -3,7 +3,7 @@ package handlers
import (
"net/http"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4"
)

View file

@ -0,0 +1,36 @@
package handlers
import (
"context"
"database/sql"
"errors"
"net/http"
"github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/internal/server/services"
)
type ApiV1WorkersConnectGETResponse struct {
Endpoint string `json:"endpoint"`
Group string `json:"group"`
}
func (h *BaseHandler) ApiV1WorkersConnectGET(c echo.Context) error {
ctx := context.Background()
cc := c.(AuthenticatedContext)
workerGroup, err := services.GetWorkerGroup(ctx, h.db, cc.Principal.Worker.Group)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return echo.NewHTTPError(http.StatusUnauthorized, "Token invalid")
}
return err
}
response := ApiV1WorkersConnectGETResponse{
Endpoint: h.config.Temporal.ServerHost,
Group: workerGroup.Id,
}
return c.JSON(http.StatusOK, response)
}

View file

@ -7,8 +7,8 @@ import (
"strings"
"time"
jwtInternal "code.tjo.space/mentos1386/zdravko/internal/jwt"
"github.com/labstack/echo/v4"
jwtInternal "github.com/mentos1386/zdravko/pkg/jwt"
)
const sessionName = "zdravko-hey"

View file

@ -1,12 +1,8 @@
# Example trigger code
trigger: |
import kv from 'zdravko/kv';
import incidents, { severity } from 'zdravko/incidents';
// Only execute on this specific targets.
export function filter(target) {
return target.tags.kind === 'http';
}
import kv from 'k6/x/zdravko/kv';
import incidents, { severity } from 'k6/x/zdravko/incidents';
import { getTarget, getMonitor, getOutcome } from 'k6/x/zdravko';
const getMinute = (date) => {
return Math.floor(date.getTime() / 1000 / 60);
@ -25,7 +21,11 @@ trigger: |
// This trigger will check if there were more than 5 issues in last
// 5 minutes, if so it will create a critical incident.
export default function (target, monitor, outcome) {
export default function () {
const target = getTarget();
const monitor = getMonitor();
const outcome = getOutcome();
// If the outcome is not failure, we close any potential incidents.
if (outcome.status !== 'FAILURE') {
incidents.close(target, monitor);
@ -62,6 +62,7 @@ trigger: |
# Example monitor code
check: |
import http from 'k6/http';
import { getTarget } from 'k6/x/zdravko';
export const options = {
thresholds: {
@ -70,12 +71,24 @@ check: |
},
};
// Filter out only HTTP targets.
export function filter(target) {
return target.tags.kind === "http";
};
// Execute the check on the targets.
export default function (target) {
http.get(target.url);
export default function () {
const { name, group, metadata } = getTarget();
console.log(`Running check for ${group}/${name}`)
http.get(metadata.spec.url);
}
filter: |
kind="Http" and metadata.spec.url!=""
target: |
kind: Http
tags:
production: "true"
spec:
url: "https://test.k6.io"
method: "GET"
headers:
User-Agent: "Zdravko"

View file

@ -4,12 +4,12 @@ import (
"embed"
"log/slog"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/kv"
"code.tjo.space/mentos1386/zdravko/internal/script"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/gorilla/sessions"
"github.com/jmoiron/sqlx"
"github.com/mentos1386/zdravko/database"
"github.com/mentos1386/zdravko/internal/config"
"github.com/mentos1386/zdravko/pkg/script"
"github.com/mentos1386/zdravko/web/templates/components"
"go.temporal.io/sdk/client"
"gopkg.in/yaml.v2"
)
@ -18,8 +18,10 @@ import (
var examplesYaml embed.FS
type examples struct {
Check string `yaml:"check"`
Check string `yaml:"check"`
Filter string `yaml:"filter"`
Trigger string `yaml:"trigger"`
Target string `yaml:"target"`
}
var Pages = []*components.Page{
@ -39,7 +41,7 @@ func GetPageByTitle(pages []*components.Page, title string) *components.Page {
type BaseHandler struct {
db *sqlx.DB
kvStore kv.KeyValueStore
kvStore database.KeyValueStore
config *config.ServerConfig
logger *slog.Logger
@ -50,7 +52,7 @@ type BaseHandler struct {
examples examples
}
func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Client, config *config.ServerConfig, logger *slog.Logger) *BaseHandler {
func NewBaseHandler(db *sqlx.DB, kvStore database.KeyValueStore, temporal client.Client, config *config.ServerConfig, logger *slog.Logger) *BaseHandler {
store := sessions.NewCookieStore([]byte(config.SessionSecret))
examples := examples{}
@ -64,7 +66,9 @@ func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Clien
}
examples.Check = script.EscapeString(examples.Check)
examples.Filter = script.EscapeString(examples.Filter)
examples.Trigger = script.EscapeString(examples.Trigger)
examples.Target = script.EscapeString(examples.Target)
return &BaseHandler{
db: db,

View file

@ -3,7 +3,7 @@ package handlers
import (
"net/http"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4"
)

View file

@ -5,30 +5,30 @@ import (
"net/http"
"time"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/services"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/internal/server/services"
"github.com/mentos1386/zdravko/web/templates/components"
)
type IndexData struct {
*components.Base
Checks map[string]ChecksAndStatus
ChecksLength int
TimeRange string
Status models.CheckStatus
Targets map[string]TargetsAndStatus
TargetsLength int
TimeRange string
Status models.TargetStatus
}
type Check struct {
type Target struct {
Name string
Visibility models.CheckVisibility
Visibility models.TargetVisibility
Group string
Status models.CheckStatus
Status models.TargetStatus
History *History
}
type HistoryItem struct {
Status models.CheckStatus
Status models.TargetStatus
Date time.Time
}
@ -37,23 +37,23 @@ type History struct {
Uptime float64
}
type ChecksAndStatus struct {
Status models.CheckStatus
Checks []*Check
type TargetsAndStatus struct {
Status models.TargetStatus
Targets []*Target
}
func getDateString(date time.Time) string {
return date.UTC().Format("2006-01-02T15:04:05")
}
func getHistory(history []*models.CheckHistory, period time.Duration, buckets int) *History {
historyMap := map[string]models.CheckStatus{}
func getHistory(history []*services.TargetHistory, period time.Duration, buckets int) *History {
historyMap := map[string]models.TargetStatus{}
numOfSuccess := 0.0
numTotal := 0.0
for i := 0; i < buckets; i++ {
dateString := getDateString(time.Now().Add(period * time.Duration(-i)).Truncate(period))
historyMap[dateString] = models.CheckStatusUnknown
historyMap[dateString] = models.TargetStatusUnknown
}
for _, _history := range history {
@ -65,16 +65,16 @@ func getHistory(history []*models.CheckHistory, period time.Duration, buckets in
}
numTotal++
if _history.Status == models.CheckStatusSuccess {
if _history.Status == models.TargetStatusSuccess {
numOfSuccess++
}
// skip if it is already set to failure
if historyMap[dateString] == models.CheckStatusFailure {
if historyMap[dateString] == models.TargetStatusFailure {
continue
}
// FIXME: This is wrong! As we can have multiple checks in dateString.
// FIXME: This is wrong! As we can have multiple targets in dateString.
// We should look at only the newest one.
historyMap[dateString] = _history.Status
}
@ -102,7 +102,7 @@ func getHistory(history []*models.CheckHistory, period time.Duration, buckets in
func (h *BaseHandler) Index(c echo.Context) error {
ctx := context.Background()
checks, err := services.GetChecks(ctx, h.db)
targets, err := services.GetTargets(ctx, h.db)
if err != nil {
return err
}
@ -112,12 +112,12 @@ func (h *BaseHandler) Index(c echo.Context) error {
timeRange = "90days"
}
overallStatus := models.CheckStatusUnknown
statusByGroup := make(map[string]models.CheckStatus)
overallStatus := models.TargetStatusUnknown
statusByGroup := make(map[string]models.TargetStatus)
checksWithHistory := make([]*Check, len(checks))
for i, check := range checks {
history, err := services.GetCheckHistoryForCheck(ctx, h.db, check.Id)
targetsWithHistory := make([]*Target, len(targets))
for i, target := range targets {
history, err := services.GetTargetHistoryForTarget(ctx, h.db, target.Id)
if err != nil {
return err
}
@ -132,38 +132,38 @@ func (h *BaseHandler) Index(c echo.Context) error {
historyResult = getHistory(history, time.Minute, 90)
}
if statusByGroup[check.Group] == "" {
statusByGroup[check.Group] = models.CheckStatusUnknown
if statusByGroup[target.Group] == "" {
statusByGroup[target.Group] = models.TargetStatusUnknown
}
status := historyResult.List[len(historyResult.List)-1]
if status.Status == models.CheckStatusSuccess {
if overallStatus == models.CheckStatusUnknown {
if status.Status == models.TargetStatusSuccess {
if overallStatus == models.TargetStatusUnknown {
overallStatus = status.Status
}
if statusByGroup[check.Group] == models.CheckStatusUnknown {
statusByGroup[check.Group] = status.Status
if statusByGroup[target.Group] == models.TargetStatusUnknown {
statusByGroup[target.Group] = status.Status
}
}
if status.Status != models.CheckStatusSuccess && status.Status != models.CheckStatusUnknown {
if status.Status != models.TargetStatusSuccess && status.Status != models.TargetStatusUnknown {
overallStatus = status.Status
statusByGroup[check.Group] = status.Status
statusByGroup[target.Group] = status.Status
}
checksWithHistory[i] = &Check{
Name: check.Name,
Visibility: check.Visibility,
Group: check.Group,
targetsWithHistory[i] = &Target{
Name: target.Name,
Visibility: target.Visibility,
Group: target.Group,
Status: status.Status,
History: historyResult,
}
}
checksByGroup := map[string]ChecksAndStatus{}
for _, check := range checksWithHistory {
checksByGroup[check.Group] = ChecksAndStatus{
Status: statusByGroup[check.Group],
Checks: append(checksByGroup[check.Group].Checks, check),
targetsByGroup := map[string]TargetsAndStatus{}
for _, target := range targetsWithHistory {
targetsByGroup[target.Group] = TargetsAndStatus{
Status: statusByGroup[target.Group],
Targets: append(targetsByGroup[target.Group].Targets, target),
}
}
@ -174,7 +174,7 @@ func (h *BaseHandler) Index(c echo.Context) error {
NavbarActive: GetPageByTitle(Pages, "Status"),
Navbar: Pages,
},
Checks: checksByGroup,
Targets: targetsByGroup,
TimeRange: timeRange,
Status: overallStatus,
})

View file

@ -12,10 +12,10 @@ import (
"strconv"
"time"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/services"
"github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/internal/config"
"github.com/mentos1386/zdravko/internal/server/services"
"golang.org/x/oauth2"
)

View file

@ -3,9 +3,9 @@ package handlers
import (
"net/http"
"code.tjo.space/mentos1386/zdravko/internal/services"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/internal/server/services"
"github.com/mentos1386/zdravko/web/templates/components"
)
type SettingsSidebarGroup struct {
@ -115,7 +115,7 @@ type SettingsHome struct {
WorkerGroupsCount int
ChecksCount int
NotificationsCount int
History []*services.CheckHistoryWithCheck
History []*services.CheckHistory
}
func (h *BaseHandler) SettingsHomeGET(c echo.Context) error {
@ -132,7 +132,7 @@ func (h *BaseHandler) SettingsHomeGET(c echo.Context) error {
return err
}
history, err := services.GetLastNCheckHistory(ctx, h.db, 10)
history, err := services.GetLastNCheckHistory(ctx, h.temporal, 10)
if err != nil {
return err
}

View file

@ -5,33 +5,31 @@ import (
"database/sql"
"fmt"
"net/http"
"slices"
"strings"
"time"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/script"
"code.tjo.space/mentos1386/zdravko/internal/services"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/go-playground/validator/v10"
"github.com/gosimple/slug"
"github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/internal/server/services"
"github.com/mentos1386/zdravko/pkg/script"
"github.com/mentos1386/zdravko/web/templates/components"
)
type CreateCheck struct {
Name string `validate:"required"`
Group string `validate:"required"`
WorkerGroups string `validate:"required"`
Schedule string `validate:"required,cron"`
Script string `validate:"required"`
Visibility string `validate:"required,oneof=PUBLIC PRIVATE"`
Filter string `validate:"required"`
}
type UpdateCheck struct {
Group string `validate:"required"`
WorkerGroups string `validate:"required"`
Schedule string `validate:"required,cron"`
Script string `validate:"required"`
Visibility string `validate:"required,oneof=PUBLIC PRIVATE"`
Filter string `validate:"required"`
}
type CheckWithWorkerGroupsAndState struct {
@ -41,19 +39,24 @@ type CheckWithWorkerGroupsAndState struct {
type SettingsChecks struct {
*Settings
Checks map[string][]*CheckWithWorkerGroupsAndState
CheckGroups []string
Checks []*CheckWithWorkerGroupsAndState
History []struct {
CreatedAt time.Time
Status string
Note string
}
}
type SettingsCheck struct {
*Settings
Check *CheckWithWorkerGroupsAndState
History []*models.CheckHistory
History []*services.CheckHistory
}
type SettingsCheckCreate struct {
*Settings
Example string
ExampleScript string
ExampleFilter string
}
func (h *BaseHandler) SettingsChecksGET(c echo.Context) error {
@ -76,23 +79,13 @@ func (h *BaseHandler) SettingsChecksGET(c echo.Context) error {
}
}
checkGroups := []string{}
checksByGroup := map[string][]*CheckWithWorkerGroupsAndState{}
for _, check := range checksWithState {
checksByGroup[check.Group] = append(checksByGroup[check.Group], check)
if !slices.Contains(checkGroups, check.Group) {
checkGroups = append(checkGroups, check.Group)
}
}
return c.Render(http.StatusOK, "settings_checks.tmpl", &SettingsChecks{
Settings: NewSettings(
cc.Principal.User,
GetPageByTitle(SettingsPages, "Checks"),
[]*components.Page{GetPageByTitle(SettingsPages, "Checks")},
),
Checks: checksByGroup,
CheckGroups: checkGroups,
Checks: checksWithState,
})
}
@ -116,7 +109,7 @@ func (h *BaseHandler) SettingsChecksDescribeGET(c echo.Context) error {
State: status,
}
history, err := services.GetCheckHistoryForCheck(context.Background(), h.db, slug)
history, err := services.GetCheckHistoryForCheck(context.Background(), h.temporal, slug)
if err != nil {
return err
}
@ -196,11 +189,10 @@ func (h *BaseHandler) SettingsChecksDescribePOST(c echo.Context) error {
checkId := c.Param("id")
update := UpdateCheck{
Group: strings.ToLower(c.FormValue("group")),
WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))),
Schedule: c.FormValue("schedule"),
Script: script.EscapeString(c.FormValue("script")),
Visibility: c.FormValue("visibility"),
Filter: c.FormValue("filter"),
}
err := validator.New(validator.WithRequiredStructEnabled()).Struct(update)
if err != nil {
@ -211,10 +203,9 @@ func (h *BaseHandler) SettingsChecksDescribePOST(c echo.Context) error {
if err != nil {
return err
}
check.Group = update.Group
check.Schedule = update.Schedule
check.Script = update.Script
check.Visibility = models.CheckVisibility(update.Visibility)
check.Filter = update.Filter
err = services.UpdateCheck(
ctx,
@ -270,7 +261,8 @@ func (h *BaseHandler) SettingsChecksCreateGET(c echo.Context) error {
GetPageByTitle(SettingsPages, "Checks Create"),
},
),
Example: h.examples.Check,
ExampleScript: h.examples.Check,
ExampleFilter: h.examples.Filter,
})
}
@ -280,11 +272,10 @@ func (h *BaseHandler) SettingsChecksCreatePOST(c echo.Context) error {
create := CreateCheck{
Name: c.FormValue("name"),
Group: strings.ToLower(c.FormValue("group")),
WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))),
Schedule: c.FormValue("schedule"),
Script: script.EscapeString(c.FormValue("script")),
Visibility: c.FormValue("visibility"),
Filter: c.FormValue("filter"),
}
err := validator.New(validator.WithRequiredStructEnabled()).Struct(create)
if err != nil {
@ -312,12 +303,11 @@ func (h *BaseHandler) SettingsChecksCreatePOST(c echo.Context) error {
}
check := &models.Check{
Name: create.Name,
Group: create.Group,
Id: checkId,
Schedule: create.Schedule,
Script: create.Script,
Visibility: models.CheckVisibility(create.Visibility),
Name: create.Name,
Id: checkId,
Schedule: create.Schedule,
Script: create.Script,
Filter: create.Filter,
}
err = services.CreateCheck(

View file

@ -3,7 +3,7 @@ package handlers
import (
"net/http"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4"
)

View file

@ -3,7 +3,7 @@ package handlers
import (
"net/http"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4"
)

View file

@ -0,0 +1,240 @@
package handlers
import (
"context"
"fmt"
"net/http"
"slices"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gosimple/slug"
"github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/internal/server/services"
"github.com/mentos1386/zdravko/web/templates/components"
)
type CreateTarget struct {
Name string `validate:"required"`
Group string `validate:"required"`
Visibility string `validate:"required,oneof=PUBLIC PRIVATE"`
Metadata string `validate:"required"`
}
type UpdateTarget struct {
Group string `validate:"required"`
Visibility string `validate:"required,oneof=PUBLIC PRIVATE"`
Metadata string `validate:"required"`
}
type SettingsTargets struct {
*Settings
Targets map[string][]*models.Target
TargetGroups []string
}
type SettingsTarget struct {
*Settings
Target *models.Target
History []*services.TargetHistory
}
type SettingsTargetCreate struct {
*Settings
Example string
}
func (h *BaseHandler) SettingsTargetsGET(c echo.Context) error {
cc := c.(AuthenticatedContext)
targets, err := services.GetTargets(context.Background(), h.db)
if err != nil {
return err
}
targetGroups := []string{}
targetsByGroup := map[string][]*models.Target{}
for _, target := range targets {
targetsByGroup[target.Group] = append(targetsByGroup[target.Group], target)
if !slices.Contains(targetGroups, target.Group) {
targetGroups = append(targetGroups, target.Group)
}
}
return c.Render(http.StatusOK, "settings_targets.tmpl", &SettingsTargets{
Settings: NewSettings(
cc.Principal.User,
GetPageByTitle(SettingsPages, "Targets"),
[]*components.Page{GetPageByTitle(SettingsPages, "Targets")},
),
Targets: targetsByGroup,
TargetGroups: targetGroups,
})
}
func (h *BaseHandler) SettingsTargetsDescribeGET(c echo.Context) error {
cc := c.(AuthenticatedContext)
slug := c.Param("id")
target, err := services.GetTarget(context.Background(), h.db, slug)
if err != nil {
return err
}
history, err := services.GetTargetHistoryForTarget(context.Background(), h.db, slug)
if err != nil {
return err
}
maxElements := 10
if len(history) < maxElements {
maxElements = len(history)
}
return c.Render(http.StatusOK, "settings_targets_describe.tmpl", &SettingsTarget{
Settings: NewSettings(
cc.Principal.User,
GetPageByTitle(SettingsPages, "Targets"),
[]*components.Page{
GetPageByTitle(SettingsPages, "Targets"),
{
Path: fmt.Sprintf("/settings/targets/%s", slug),
Title: "Describe",
Breadcrumb: target.Name,
},
}),
Target: target,
History: history[:maxElements],
})
}
func (h *BaseHandler) SettingsTargetsDescribeDELETE(c echo.Context) error {
slug := c.Param("id")
err := services.DeleteTarget(context.Background(), h.db, slug)
if err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, "/settings/targets")
}
func (h *BaseHandler) SettingsTargetsDisableGET(c echo.Context) error {
slug := c.Param("id")
target, err := services.GetTarget(context.Background(), h.db, slug)
if err != nil {
return err
}
err = services.SetTargetState(context.Background(), h.db, target.Id, models.TargetStatePaused)
if err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/targets/%s", slug))
}
func (h *BaseHandler) SettingsTargetsEnableGET(c echo.Context) error {
slug := c.Param("id")
target, err := services.GetTarget(context.Background(), h.db, slug)
if err != nil {
return err
}
err = services.SetTargetState(context.Background(), h.db, target.Id, models.TargetStateActive)
if err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/targets/%s", slug))
}
func (h *BaseHandler) SettingsTargetsDescribePOST(c echo.Context) error {
ctx := context.Background()
targetId := c.Param("id")
update := UpdateTarget{
Group: strings.ToLower(c.FormValue("group")),
Visibility: c.FormValue("visibility"),
Metadata: c.FormValue("metadata"),
}
err := validator.New(validator.WithRequiredStructEnabled()).Struct(update)
if err != nil {
return err
}
target, err := services.GetTarget(ctx, h.db, targetId)
if err != nil {
return err
}
target.Group = update.Group
target.Visibility = models.TargetVisibility(update.Visibility)
target.Metadata = update.Metadata
err = services.UpdateTarget(
ctx,
h.db,
target,
)
if err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/targets/%s", targetId))
}
func (h *BaseHandler) SettingsTargetsCreateGET(c echo.Context) error {
cc := c.(AuthenticatedContext)
return c.Render(http.StatusOK, "settings_targets_create.tmpl", &SettingsTargetCreate{
Settings: NewSettings(
cc.Principal.User,
GetPageByTitle(SettingsPages, "Targets"),
[]*components.Page{
GetPageByTitle(SettingsPages, "Targets"),
GetPageByTitle(SettingsPages, "Targets Create"),
},
),
Example: h.examples.Target,
})
}
func (h *BaseHandler) SettingsTargetsCreatePOST(c echo.Context) error {
ctx := context.Background()
targetId := slug.Make(c.FormValue("name"))
create := CreateTarget{
Name: c.FormValue("name"),
Group: strings.ToLower(c.FormValue("group")),
Visibility: c.FormValue("visibility"),
Metadata: c.FormValue("metadata"),
}
err := validator.New(validator.WithRequiredStructEnabled()).Struct(create)
if err != nil {
return err
}
target := &models.Target{
Name: create.Name,
Group: create.Group,
Id: targetId,
Visibility: models.TargetVisibility(create.Visibility),
State: models.TargetStateActive,
Metadata: create.Metadata,
}
err = services.CreateTarget(
ctx,
h.db,
target,
)
if err != nil {
return err
}
return c.Redirect(http.StatusSeeOther, "/settings/targets")
}

View file

@ -5,13 +5,13 @@ import (
"fmt"
"net/http"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/script"
"code.tjo.space/mentos1386/zdravko/internal/services"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/go-playground/validator/v10"
"github.com/gosimple/slug"
"github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/internal/server/services"
"github.com/mentos1386/zdravko/pkg/script"
"github.com/mentos1386/zdravko/web/templates/components"
)
type CreateTrigger struct {
@ -36,7 +36,7 @@ type SettingsTriggers struct {
type SettingsTrigger struct {
*Settings
Trigger *TriggerWithState
History []*models.TriggerHistory
History []*services.TriggerHistory
}
type SettingsTriggerCreate struct {

View file

@ -6,13 +6,13 @@ import (
"net/http"
"strings"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/jwt"
"code.tjo.space/mentos1386/zdravko/internal/services"
"code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/go-playground/validator/v10"
"github.com/gosimple/slug"
"github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/internal/server/services"
"github.com/mentos1386/zdravko/pkg/jwt"
"github.com/mentos1386/zdravko/web/templates/components"
)
type WorkerWithTokenAndActiveWorkers struct {
@ -52,7 +52,7 @@ func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error {
}
workerGroupsWithActiveWorkers[i] = &WorkerGroupWithActiveWorkers{
WorkerGroupWithChecks: workerGroup,
ActiveWorkers: activeWorkers,
ActiveWorkers: activeWorkers,
}
}

View file

@ -5,8 +5,8 @@ import (
"net/http/httputil"
"net/url"
"code.tjo.space/mentos1386/zdravko/internal/jwt"
"github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/pkg/jwt"
)
func (h *BaseHandler) Temporal(c echo.Context) error {

View file

@ -6,9 +6,9 @@ import (
"sort"
"time"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/workflows"
"github.com/jmoiron/sqlx"
"github.com/mentos1386/zdravko/database/models"
internaltemporal "github.com/mentos1386/zdravko/internal/temporal"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/temporal"
"golang.org/x/exp/maps"
@ -55,8 +55,8 @@ func SetCheckState(ctx context.Context, temporal client.Client, id string, state
func CreateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error {
_, err := db.NamedExecContext(ctx,
`INSERT INTO checks (id, name, visibility, "group", script, schedule)
VALUES (:id, :name, :visibility, :group, :script, :schedule)`,
`INSERT INTO checks (id, name, script, schedule, filter)
VALUES (:id, :name, :script, :schedule, :filter)`,
check,
)
return err
@ -64,7 +64,7 @@ func CreateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error {
func UpdateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error {
_, err := db.NamedExecContext(ctx,
`UPDATE checks SET visibility=:visibility, "group"=:group, script=:script, schedule=:schedule WHERE id=:id`,
`UPDATE checks SET script=:script, schedule=:schedule, filter=:filter WHERE id=:id`,
check,
)
return err
@ -120,12 +120,11 @@ func GetCheckWithWorkerGroups(ctx context.Context, db *sqlx.DB, id string) (*mod
SELECT
checks.id,
checks.name,
checks.visibility,
checks."group",
checks.script,
checks.schedule,
checks.created_at,
checks.updated_at,
checks.filter,
worker_groups.name as worker_group_name
FROM checks
LEFT OUTER JOIN check_worker_groups ON checks.id = check_worker_groups.check_id
@ -147,12 +146,11 @@ ORDER BY checks.name
err = rows.Scan(
&check.Id,
&check.Name,
&check.Visibility,
&check.Group,
&check.Script,
&check.Schedule,
&check.CreatedAt,
&check.UpdatedAt,
&check.Filter,
&workerGroupName,
)
if err != nil {
@ -180,12 +178,11 @@ func GetChecksWithWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.Chec
SELECT
checks.id,
checks.name,
checks.visibility,
checks."group",
checks.script,
checks.schedule,
checks.created_at,
checks.updated_at,
checks.filter,
worker_groups.name as worker_group_name
FROM checks
LEFT OUTER JOIN check_worker_groups ON checks.id = check_worker_groups.check_id
@ -206,12 +203,11 @@ ORDER BY checks.name
err = rows.Scan(
&check.Id,
&check.Name,
&check.Visibility,
&check.Group,
&check.Script,
&check.Schedule,
&check.CreatedAt,
&check.UpdatedAt,
&check.Filter,
&workerGroupName,
)
if err != nil {
@ -254,7 +250,8 @@ func CreateOrUpdateCheckSchedule(
}
args := make([]interface{}, 1)
args[0] = workflows.CheckWorkflowParam{
args[0] = internaltemporal.WorkflowCheckParam{
Filter: check.Filter,
Script: check.Script,
CheckId: check.Id,
WorkerGroupIds: workerGroupStrings,
@ -268,7 +265,7 @@ func CreateOrUpdateCheckSchedule(
},
Action: &client.ScheduleWorkflowAction{
ID: getScheduleId(check.Id),
Workflow: workflows.NewWorkflows(nil).CheckWorkflowDefinition,
Workflow: internaltemporal.WorkflowCheckName,
Args: args,
TaskQueue: "default",
RetryPolicy: &temporal.RetryPolicy{

View file

@ -0,0 +1,67 @@
package services
import (
"context"
"fmt"
"time"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/client"
)
type CheckHistory struct {
CheckId string
Status string
Duration time.Duration
}
func GetLastNCheckHistory(ctx context.Context, temporal client.Client, n int32) ([]*CheckHistory, error) {
var checkHistory []*CheckHistory
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
PageSize: n,
})
if err != nil {
return checkHistory, err
}
executions := response.GetExecutions()
for _, execution := range executions {
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
checkId := scheduleId[len("check-"):]
checkHistory = append(checkHistory, &CheckHistory{
CheckId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
Status: execution.Status.String(),
})
}
return checkHistory, nil
}
func GetCheckHistoryForCheck(ctx context.Context, temporal client.Client, checkId string) ([]*CheckHistory, error) {
var checkHistory []*CheckHistory
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
PageSize: 10,
Query: fmt.Sprintf(`TemporalScheduledById = "%s"`, getScheduleId(checkId)),
})
if err != nil {
return checkHistory, err
}
executions := response.GetExecutions()
for _, execution := range executions {
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
checkId := scheduleId[len("check-"):]
checkHistory = append(checkHistory, &CheckHistory{
CheckId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
Status: execution.Status.String(),
})
}
return checkHistory, nil
}

View file

@ -3,7 +3,7 @@ package services
import (
"context"
"code.tjo.space/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx"
)

View file

@ -0,0 +1,75 @@
package services
import (
"context"
"github.com/jmoiron/sqlx"
"github.com/mentos1386/zdravko/database/models"
)
func CountTargets(ctx context.Context, db *sqlx.DB) (int, error) {
var count int
err := db.GetContext(ctx, &count, "SELECT COUNT(*) FROM targets")
return count, err
}
func SetTargetState(ctx context.Context, db *sqlx.DB, id string, state models.TargetState) error {
_, err := db.NamedExecContext(ctx,
`UPDATE targets SET state=:state WHERE id=:id`,
struct {
Id string
State models.TargetState
}{Id: id, State: state},
)
return err
}
func CreateTarget(ctx context.Context, db *sqlx.DB, target *models.Target) error {
_, err := db.NamedExecContext(ctx,
`INSERT INTO targets (id, name, "group", visibility, state, metadata) VALUES (:id, :name, :group, :visibility, :state, :metadata)`,
target,
)
return err
}
func UpdateTarget(ctx context.Context, db *sqlx.DB, target *models.Target) error {
_, err := db.NamedExecContext(ctx,
`UPDATE targets SET visibility=:visibility, "group"=:group, metadata=:metadata WHERE id=:id`,
target,
)
return err
}
func DeleteTarget(ctx context.Context, db *sqlx.DB, id string) error {
_, err := db.ExecContext(ctx,
"DELETE FROM targets WHERE id=$1",
id,
)
return err
}
func GetTarget(ctx context.Context, db *sqlx.DB, id string) (*models.Target, error) {
target := &models.Target{}
err := db.GetContext(ctx, target,
"SELECT * FROM targets WHERE id=$1",
id,
)
return target, err
}
func GetTargets(ctx context.Context, db *sqlx.DB) ([]*models.Target, error) {
targets := []*models.Target{}
err := db.SelectContext(ctx, &targets,
"SELECT * FROM targets ORDER BY name",
)
return targets, err
}
func GetTargetsWithFilter(ctx context.Context, db *sqlx.DB, filter string) ([]*models.Target, error) {
targets := []*models.Target{}
err := db.SelectContext(ctx, &targets,
"SELECT * FROM targets WHERE name ILIKE $1 ORDER BY name",
"%"+filter+"%",
)
return targets, err
}

View file

@ -0,0 +1,59 @@
package services
import (
"context"
"github.com/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx"
)
type TargetHistory struct {
*models.TargetHistory
TargetName string `db:"target_name"`
}
func GetLastNTargetHistory(ctx context.Context, db *sqlx.DB, n int) ([]*TargetHistory, error) {
var targetHistory []*TargetHistory
err := db.SelectContext(ctx, &targetHistory, `
SELECT
th.*,
t.name AS target_name
FROM target_histories th
LEFT JOIN targets t ON th.target_id = t.id
WHERE th.target_id = $1
ORDER BY th.created_at DESC
LIMIT $1
`, n)
return targetHistory, err
}
func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string) ([]*TargetHistory, error) {
var targetHistory []*TargetHistory
err := db.SelectContext(ctx, &targetHistory, `
SELECT
th.*,
t.name AS target_name
FROM target_histories th
LEFT JOIN targets t ON th.target_id = t.id
WHERE th.target_id = $1
ORDER BY th.created_at DESC
`, targetId)
return targetHistory, err
}
func AddHistoryForTarget(ctx context.Context, db *sqlx.DB, history *models.TargetHistory) error {
_, err := db.NamedExecContext(ctx,
`
INSERT INTO target_histories (
target_id,
status,
note
) VALUES (
:target_id,
:status,
:note
)`,
history,
)
return err
}

View file

@ -3,7 +3,7 @@ package services
import (
"context"
"code.tjo.space/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx"
)

View file

@ -0,0 +1,67 @@
package services
import (
"context"
"fmt"
"time"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/client"
)
type TriggerHistory struct {
TriggerId string
Status string
Duration time.Duration
}
func GetLastNTriggerHistory(ctx context.Context, temporal client.Client, n int32) ([]*TriggerHistory, error) {
var checkHistory []*TriggerHistory
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
PageSize: n,
})
if err != nil {
return checkHistory, err
}
executions := response.GetExecutions()
for _, execution := range executions {
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
checkId := scheduleId[len("trigger-"):]
checkHistory = append(checkHistory, &TriggerHistory{
TriggerId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
Status: execution.Status.String(),
})
}
return checkHistory, nil
}
func GetTriggerHistoryForTrigger(ctx context.Context, temporal client.Client, checkId string) ([]*TriggerHistory, error) {
var checkHistory []*TriggerHistory
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
PageSize: 10,
Query: fmt.Sprintf(`TemporalScheduledById = "%s"`, getScheduleId(checkId)),
})
if err != nil {
return checkHistory, err
}
executions := response.GetExecutions()
for _, execution := range executions {
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
checkId := scheduleId[len("check-"):]
checkHistory = append(checkHistory, &TriggerHistory{
TriggerId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
Status: execution.Status.String(),
})
}
return checkHistory, nil
}

View file

@ -3,7 +3,7 @@ package services
import (
"context"
"code.tjo.space/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx"
"go.temporal.io/api/enums/v1"
"go.temporal.io/sdk/client"

View file

@ -0,0 +1,58 @@
package workflows
import (
"log/slog"
"sort"
"time"
"github.com/mentos1386/zdravko/internal/temporal"
"github.com/mentos1386/zdravko/pkg/api"
"go.temporal.io/sdk/workflow"
)
func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal.WorkflowCheckParam) (api.CheckStatus, error) {
workerGroupIds := param.WorkerGroupIds
sort.Strings(workerGroupIds)
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second,
TaskQueue: temporal.TEMPORAL_SERVER_QUEUE,
})
targetsFilterParam := temporal.ActivityTargetsFilterParam{
Filter: param.Filter,
}
targetsFilterResult := temporal.ActivityTargetsFilterResult{}
err := workflow.ExecuteActivity(ctx, temporal.ActivityTargetsFilterName, targetsFilterParam).Get(ctx, &targetsFilterResult)
if err != nil {
return api.CheckStatusUnknown, err
}
for _, target := range targetsFilterResult.Targets {
for _, workerGroupId := range workerGroupIds {
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second,
TaskQueue: workerGroupId,
})
heatlcheckParam := temporal.ActivityCheckParam{
Script: param.Script,
Target: target,
}
var checkResult *temporal.ActivityCheckResult
err := workflow.ExecuteActivity(ctx, temporal.ActivityCheckName, heatlcheckParam).Get(ctx, &checkResult)
if err != nil {
return api.CheckStatusUnknown, err
}
status := api.CheckStatusFailure
if checkResult.Success {
status = api.CheckStatusSuccess
}
slog.Info("Check %s status: %s", param.CheckId, status)
}
}
return api.CheckStatusSuccess, nil
}

View file

@ -0,0 +1,9 @@
package workflows
type Workflows struct {
}
func NewWorkflows() *Workflows {
return &Workflows{}
}

View file

@ -1,67 +0,0 @@
package services
import (
"context"
"code.tjo.space/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx"
)
type CheckHistoryWithCheck struct {
*models.CheckHistory
CheckName string `db:"check_name"`
CheckId string `db:"check_id"`
}
func GetLastNCheckHistory(ctx context.Context, db *sqlx.DB, n int) ([]*CheckHistoryWithCheck, error) {
var checkHistory []*CheckHistoryWithCheck
err := db.SelectContext(ctx, &checkHistory, `
SELECT
mh.*,
wg.name AS worker_group_name,
m.name AS check_name,
m.id AS check_id
FROM check_histories mh
LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id
LEFT JOIN check_worker_groups mwg ON mh.check_id = mwg.check_id
LEFT JOIN checks m ON mwg.check_id = m.id
ORDER BY mh.created_at DESC
LIMIT $1
`, n)
return checkHistory, err
}
func GetCheckHistoryForCheck(ctx context.Context, db *sqlx.DB, checkId string) ([]*models.CheckHistory, error) {
var checkHistory []*models.CheckHistory
err := db.SelectContext(ctx, &checkHistory, `
SELECT
mh.*,
wg.name AS worker_group_name,
wg.id AS worker_group_id
FROM check_histories as mh
LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id
LEFT JOIN check_worker_groups mwg ON mh.check_id = mwg.check_id
WHERE mh.check_id = $1
ORDER BY mh.created_at DESC
`, checkId)
return checkHistory, err
}
func AddHistoryForCheck(ctx context.Context, db *sqlx.DB, history *models.CheckHistory) error {
_, err := db.NamedExecContext(ctx,
`
INSERT INTO check_histories (
check_id,
worker_group_id,
status,
note
) VALUES (
:check_id,
:worker_group_id,
:status,
:note
)`,
history,
)
return err
}

View file

@ -0,0 +1,15 @@
package temporal
import "github.com/mentos1386/zdravko/database/models"
type ActivityCheckParam struct {
Script string
Target *models.Target
}
type ActivityCheckResult struct {
Success bool
Note string
}
const ActivityCheckName = "CHECK"

View file

@ -0,0 +1,10 @@
package temporal
type ActivityProcessCheckOutcomeParam struct {
Outcome string
}
type ActivityProcessCheckOutcomeResult struct {
}
const ActivityProcessCheckOutcomeName = "PROCESS_CHECK_OUTCOME"

View file

@ -0,0 +1,15 @@
package temporal
import (
"github.com/mentos1386/zdravko/database/models"
)
type ActivityTargetsFilterParam struct {
Filter string
}
type ActivityTargetsFilterResult struct {
Targets []*models.Target
}
const ActivityTargetsFilterName = "TARGETS_FILTER"

View file

@ -5,13 +5,17 @@ import (
"log/slog"
"time"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/jwt"
"code.tjo.space/mentos1386/zdravko/pkg/retry"
"github.com/mentos1386/zdravko/internal/config"
"github.com/mentos1386/zdravko/pkg/jwt"
"github.com/mentos1386/zdravko/pkg/retry"
"github.com/pkg/errors"
"go.temporal.io/sdk/client"
)
// Must be default, as we are also processing
// some temporal things.
const TEMPORAL_SERVER_QUEUE = "default"
type AuthHeadersProvider struct {
Token string
}

View file

@ -0,0 +1,10 @@
package temporal
type WorkflowCheckParam struct {
Script string
Filter string
CheckId string
WorkerGroupIds []string
}
const WorkflowCheckName = "CHECK_WORKFLOW"

View file

@ -0,0 +1,16 @@
package activities
import (
"log/slog"
"github.com/mentos1386/zdravko/internal/config"
)
type Activities struct {
config *config.WorkerConfig
logger *slog.Logger
}
func NewActivities(config *config.WorkerConfig, logger *slog.Logger) *Activities {
return &Activities{config: config, logger: logger}
}

View file

@ -0,0 +1,21 @@
package activities
import (
"context"
"log/slog"
"github.com/mentos1386/zdravko/internal/temporal"
"github.com/mentos1386/zdravko/pkg/k6"
"github.com/mentos1386/zdravko/pkg/script"
)
func (a *Activities) Check(ctx context.Context, param temporal.ActivityCheckParam) (*temporal.ActivityCheckResult, error) {
execution := k6.NewExecution(slog.Default(), script.UnescapeString(param.Script))
result, err := execution.Run(ctx)
if err != nil {
return nil, err
}
return &temporal.ActivityCheckResult{Success: result.Success, Note: result.Note}, nil
}

View file

@ -0,0 +1,8 @@
package workflows
type Workflows struct {
}
func NewWorkflows() *Workflows {
return &Workflows{}
}

View file

@ -1,58 +0,0 @@
package workflows
import (
"sort"
"time"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/activities"
"go.temporal.io/sdk/workflow"
)
type CheckWorkflowParam struct {
Script string
CheckId string
WorkerGroupIds []string
}
func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param CheckWorkflowParam) (models.CheckStatus, error) {
workerGroupIds := param.WorkerGroupIds
sort.Strings(workerGroupIds)
for _, workerGroupId := range workerGroupIds {
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second,
TaskQueue: workerGroupId,
})
heatlcheckParam := activities.HealtcheckParam{
Script: param.Script,
}
var checkResult *activities.CheckResult
err := workflow.ExecuteActivity(ctx, w.activities.Check, heatlcheckParam).Get(ctx, &checkResult)
if err != nil {
return models.CheckStatusUnknown, err
}
status := models.CheckStatusFailure
if checkResult.Success {
status = models.CheckStatusSuccess
}
historyParam := activities.HealtcheckAddToHistoryParam{
CheckId: param.CheckId,
Status: status,
Note: checkResult.Note,
WorkerGroupId: workerGroupId,
}
var historyResult *activities.CheckAddToHistoryResult
err = workflow.ExecuteActivity(ctx, w.activities.CheckAddToHistory, historyParam).Get(ctx, &historyResult)
if err != nil {
return models.CheckStatusUnknown, err
}
}
return models.CheckStatusSuccess, nil
}

View file

@ -1,13 +0,0 @@
package workflows
import (
"code.tjo.space/mentos1386/zdravko/internal/activities"
)
type Workflows struct {
activities *activities.Activities
}
func NewWorkflows(a *activities.Activities) *Workflows {
return &Workflows{activities: a}
}

View file

@ -103,7 +103,7 @@ migration-new name:
echo "Created migration file: $FILENAME"
# Generate and download all external dependencies.
generate:
generate: _tailwindcss-build _htmx-download _monaco-download _feather-icons-download
go generate ./...
_tailwindcss-build:
@ -119,11 +119,12 @@ _monaco-download:
mv node_modules/monaco-editor/min {{STATIC_DIR}}/monaco
rm -rf node_modules
# We onlt care about javascript language
# We only care about javascript language
find {{STATIC_DIR}}/monaco/vs/basic-languages/ \
-type d \
-not -name 'javascript' \
-not -name 'typescript' \
-not -name 'yaml' \
-not -name 'basic-languages' \
-prune -exec rm -rf {} \;

View file

@ -1,9 +1,15 @@
package api
import "code.tjo.space/mentos1386/zdravko/database/models"
type CheckStatus string
const (
CheckStatusSuccess CheckStatus = "SUCCESS"
CheckStatusFailure CheckStatus = "FAILURE"
CheckStatusUnknown CheckStatus = "UNKNOWN"
)
type ApiV1ChecksHistoryPOSTBody struct {
Status models.CheckStatus `json:"status"`
Note string `json:"note"`
WorkerGroupId string `json:"worker_group"`
Status CheckStatus `json:"status"`
Note string `json:"note"`
WorkerGroupId string `json:"worker_group"`
}

View file

@ -6,7 +6,7 @@ import (
"encoding/hex"
"time"
"code.tjo.space/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/database/models"
"github.com/golang-jwt/jwt/v5"
"github.com/pkg/errors"
)

View file

@ -5,6 +5,8 @@ import (
"log/slog"
"os"
"testing"
"github.com/mentos1386/zdravko/pkg/k6/zdravko"
)
func getLogger() *slog.Logger {
@ -18,12 +20,12 @@ func getLogger() *slog.Logger {
}
func TestK6Success(t *testing.T) {
ctx := context.Background()
logger := getLogger()
script := `
import http from 'k6/http';
import { sleep } from 'k6';
import { getTarget } from 'k6/x/zdravko';
export const options = {
vus: 10,
@ -31,6 +33,8 @@ export const options = {
};
export default function () {
const target = getTarget();
console.log('Target:', target);
http.get('https://test.k6.io');
sleep(1);
}
@ -38,6 +42,14 @@ export default function () {
execution := NewExecution(logger, script)
ctx := zdravko.WithZdravkoContext(context.Background(), zdravko.Context{Target: zdravko.Target{
Name: "Test",
Group: "Test",
Metadata: map[string]interface{}{
"Kind": "Test",
},
}})
result, err := execution.Run(ctx)
if err != nil {
t.Errorf("Error starting execution: %v", err)

23
pkg/k6/zdravko/context.go Normal file
View file

@ -0,0 +1,23 @@
package zdravko
import "context"
type zdravkoContextKey string
type Target struct {
Name string
Group string
Metadata map[string]interface{}
}
type Context struct {
Target Target
}
func WithZdravkoContext(ctx context.Context, zdravkoContext Context) context.Context {
return context.WithValue(ctx, zdravkoContextKey("zdravko-ctx"), zdravkoContext)
}
func GetZdravkoContext(ctx context.Context) Context {
return ctx.Value(zdravkoContextKey("zdravko-ctx")).(Context)
}

59
pkg/k6/zdravko/zdravko.go Normal file
View file

@ -0,0 +1,59 @@
package zdravko
import (
"go.k6.io/k6/js/modules"
)
func init() {
modules.Register("k6/x/zdravko", New())
}
type (
// RootModule is the global module instance that will create module
// instances for each VU.
RootModule struct{}
// ModuleInstance represents an instance of the JS module.
ModuleInstance struct {
// vu provides methods for accessing internal k6 objects for a VU
vu modules.VU
// comparator is the exported type
zdravko *Zdravko
}
)
// Ensure the interfaces are implemented correctly.
var (
_ modules.Instance = &ModuleInstance{}
_ modules.Module = &RootModule{}
)
// New returns a pointer to a new RootModule instance.
func New() *RootModule {
return &RootModule{}
}
// NewModuleInstance implements the modules.Module interface returning a new instance for each VU.
func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
return &ModuleInstance{
vu: vu,
zdravko: &Zdravko{vu: vu},
}
}
type Zdravko struct {
vu modules.VU
Targets []string
}
func (z *Zdravko) GetTarget() Target {
zdravkoContext := GetZdravkoContext(z.vu.Context())
return zdravkoContext.Target
}
// Exports implements the modules.Instance interface and returns the exported types for the JS module.
func (mi *ModuleInstance) Exports() modules.Exports {
return modules.Exports{
Default: mi.zdravko,
}
}

View file

@ -4,20 +4,20 @@ import (
"log/slog"
"net/http"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/handlers"
"code.tjo.space/mentos1386/zdravko/internal/kv"
"code.tjo.space/mentos1386/zdravko/web/static"
"github.com/jmoiron/sqlx"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/mentos1386/zdravko/database"
"github.com/mentos1386/zdravko/internal/config"
"github.com/mentos1386/zdravko/internal/server/handlers"
"github.com/mentos1386/zdravko/web/static"
"go.temporal.io/sdk/client"
)
func Routes(
e *echo.Echo,
sqlDb *sqlx.DB,
kvStore kv.KeyValueStore,
kvStore database.KeyValueStore,
temporalClient client.Client,
cfg *config.ServerConfig,
logger *slog.Logger,
@ -64,13 +64,13 @@ func Routes(
settings.GET("/triggers/:id/enable", h.SettingsTriggersEnableGET)
settings.GET("/targets", h.SettingsTargetsGET)
//settings.GET("/targets/create", h.SettingsTargetsCreateGET)
//settings.POST("/targets/create", h.SettingsTargetsCreatePOST)
//settings.GET("/targets/:id", h.SettingsTargetsDescribeGET)
//settings.POST("/targets/:id", h.SettingsTargetsDescribePOST)
//settings.GET("/targets/:id/delete", h.SettingsTargetsDescribeDELETE)
//settings.GET("/targets/:id/disable", h.SettingsTargetsDisableGET)
//settings.GET("/targets/:id/enable", h.SettingsTargetsEnableGET)
settings.GET("/targets/create", h.SettingsTargetsCreateGET)
settings.POST("/targets/create", h.SettingsTargetsCreatePOST)
settings.GET("/targets/:id", h.SettingsTargetsDescribeGET)
settings.POST("/targets/:id", h.SettingsTargetsDescribePOST)
settings.GET("/targets/:id/delete", h.SettingsTargetsDescribeDELETE)
settings.GET("/targets/:id/disable", h.SettingsTargetsDisableGET)
settings.GET("/targets/:id/enable", h.SettingsTargetsEnableGET)
settings.GET("/incidents", h.SettingsIncidentsGET)
@ -103,7 +103,6 @@ func Routes(
apiv1 := e.Group("/api/v1")
apiv1.Use(h.Authenticated)
apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET)
apiv1.POST("/checks/:id/history", h.ApiV1ChecksHistoryPOST)
// Error handler
e.HTTPErrorHandler = func(err error, c echo.Context) {

View file

@ -4,13 +4,12 @@ import (
"context"
"log/slog"
"code.tjo.space/mentos1386/zdravko/database"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/kv"
"code.tjo.space/mentos1386/zdravko/internal/temporal"
"code.tjo.space/mentos1386/zdravko/web/templates"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/mentos1386/zdravko/database"
"github.com/mentos1386/zdravko/internal/config"
"github.com/mentos1386/zdravko/internal/temporal"
"github.com/mentos1386/zdravko/web/templates"
"github.com/pkg/errors"
)
@ -45,12 +44,12 @@ func (s *Server) Start() error {
return errors.Wrap(err, "failed to connect to temporal")
}
kvStore, err := kv.NewBadgerKeyValueStore(s.cfg.KeyValueDatabasePath)
kvStore, err := database.NewBadgerKeyValueStore(s.cfg.KeyValueDatabasePath)
if err != nil {
return errors.Wrap(err, "failed to open kv store")
}
s.worker = NewWorker(temporalClient, s.cfg)
s.worker = NewWorker(temporalClient, s.cfg, s.logger, sqliteDb, kvStore)
s.echo.Renderer = templates.NewTemplates()
s.echo.Use(middleware.Logger())

View file

@ -1,34 +1,45 @@
package server
import (
"code.tjo.space/mentos1386/zdravko/internal/activities"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/workflows"
"log/slog"
"github.com/jmoiron/sqlx"
"github.com/mentos1386/zdravko/database"
"github.com/mentos1386/zdravko/internal/config"
"github.com/mentos1386/zdravko/internal/server/activities"
"github.com/mentos1386/zdravko/internal/server/workflows"
"github.com/mentos1386/zdravko/internal/temporal"
"go.temporal.io/sdk/activity"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
temporalWorker "go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"
)
type Worker struct {
worker worker.Worker
worker temporalWorker.Worker
}
func NewWorker(temporalClient client.Client, cfg *config.ServerConfig) *Worker {
w := worker.New(temporalClient, "default", worker.Options{})
func NewWorker(temporalClient client.Client, cfg *config.ServerConfig, logger *slog.Logger, db *sqlx.DB, kvStore database.KeyValueStore) *Worker {
worker := temporalWorker.New(temporalClient, temporal.TEMPORAL_SERVER_QUEUE, temporalWorker.Options{})
workerActivities := activities.NewActivities(&config.WorkerConfig{})
a := activities.NewActivities(cfg, logger, db, kvStore)
workerWorkflows := workflows.NewWorkflows(workerActivities)
w := workflows.NewWorkflows()
// Register Workflows
w.RegisterWorkflow(workerWorkflows.CheckWorkflowDefinition)
worker.RegisterWorkflowWithOptions(w.CheckWorkflowDefinition, workflow.RegisterOptions{Name: temporal.WorkflowCheckName})
// Register Activities
worker.RegisterActivityWithOptions(a.TargetsFilter, activity.RegisterOptions{Name: temporal.ActivityTargetsFilterName})
worker.RegisterActivityWithOptions(a.ProcessCheckOutcome, activity.RegisterOptions{Name: temporal.ActivityProcessCheckOutcomeName})
return &Worker{
worker: w,
worker: worker,
}
}
func (w *Worker) Start() error {
return w.worker.Run(worker.InterruptCh())
return w.worker.Run(temporalWorker.InterruptCh())
}
func (w *Worker) Stop() {

View file

@ -6,8 +6,8 @@ import (
"fmt"
"time"
internal "code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/jwt"
internal "github.com/mentos1386/zdravko/internal/config"
"github.com/mentos1386/zdravko/pkg/jwt"
"go.temporal.io/server/common/cluster"
"go.temporal.io/server/common/config"
"go.temporal.io/server/common/persistence/sql/sqlplugin/sqlite"

View file

@ -16,7 +16,7 @@ import (
func NewServer(cfg *config.Config, tokenKeyProvider authorization.TokenKeyProvider) (t.Server, error) {
logger := log.NewZapLogger(log.BuildZapLogger(log.Config{
Stdout: true,
Level: "info",
Level: "warn",
OutputFile: "",
}))

View file

@ -1,7 +1,7 @@
package temporal
import (
"code.tjo.space/mentos1386/zdravko/internal/config"
"github.com/mentos1386/zdravko/internal/config"
"github.com/temporalio/ui-server/v2/server"
t "go.temporal.io/server/temporal"
)

View file

@ -1,7 +1,7 @@
package temporal
import (
internal "code.tjo.space/mentos1386/zdravko/internal/config"
internal "github.com/mentos1386/zdravko/internal/config"
"github.com/temporalio/ui-server/v2/server"
"github.com/temporalio/ui-server/v2/server/config"
"github.com/temporalio/ui-server/v2/server/server_options"

View file

@ -3,17 +3,17 @@ package worker
import (
"encoding/json"
"io"
"log"
"log/slog"
"net/http"
"time"
"code.tjo.space/mentos1386/zdravko/internal/activities"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/temporal"
"code.tjo.space/mentos1386/zdravko/internal/workflows"
"code.tjo.space/mentos1386/zdravko/pkg/api"
"code.tjo.space/mentos1386/zdravko/pkg/retry"
"github.com/mentos1386/zdravko/internal/config"
"github.com/mentos1386/zdravko/internal/temporal"
"github.com/mentos1386/zdravko/internal/worker/activities"
"github.com/mentos1386/zdravko/pkg/api"
"github.com/mentos1386/zdravko/pkg/retry"
"github.com/pkg/errors"
"go.temporal.io/sdk/activity"
"go.temporal.io/sdk/worker"
)
@ -60,11 +60,13 @@ func getConnectionConfig(token string, apiUrl string) (*ConnectionConfig, error)
type Worker struct {
worker worker.Worker
cfg *config.WorkerConfig
logger *slog.Logger
}
func NewWorker(cfg *config.WorkerConfig) (*Worker, error) {
return &Worker{
cfg: cfg,
cfg: cfg,
logger: slog.Default().WithGroup("worker"),
}, nil
}
@ -78,7 +80,7 @@ func (w *Worker) Start() error {
return err
}
log.Println("Worker Group:", config.Group)
w.logger.Info("Worker Starting", "group", config.Group)
temporalClient, err := temporal.ConnectWorkerToTemporal(w.cfg.Token, config.Endpoint)
if err != nil {
@ -88,15 +90,10 @@ func (w *Worker) Start() error {
// Create a new Worker
w.worker = worker.New(temporalClient, config.Group, worker.Options{})
workerActivities := activities.NewActivities(w.cfg)
workerWorkflows := workflows.NewWorkflows(workerActivities)
// Register Workflows
w.worker.RegisterWorkflow(workerWorkflows.CheckWorkflowDefinition)
workerActivities := activities.NewActivities(w.cfg, w.logger)
// Register Activities
w.worker.RegisterActivity(workerActivities.Check)
w.worker.RegisterActivity(workerActivities.CheckAddToHistory)
w.worker.RegisterActivityWithOptions(workerActivities.Check, activity.RegisterOptions{Name: temporal.ActivityCheckName})
return w.worker.Run(worker.InterruptCh())
}

View file

@ -64,11 +64,11 @@ code {
@apply bg-blue-700 text-white;
}
.checks .time-range > a {
.targets .time-range > a {
@apply font-medium text-sm px-2.5 py-1 rounded-lg;
@apply text-black bg-gray-100 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400;
}
.checks .time-range > a.active {
.targets .time-range > a.active {
@apply bg-white hover:bg-gray-300 shadow;
}

View file

@ -722,6 +722,10 @@ video {
display: none;
}
.h-12 {
height: 3rem;
}
.h-20 {
height: 5rem;
}
@ -1278,6 +1282,10 @@ video {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@ -1408,7 +1416,7 @@ code {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.checks .time-range > a {
.targets .time-range > a {
border-radius: 0.5rem;
padding-left: 0.625rem;
padding-right: 0.625rem;
@ -1423,12 +1431,12 @@ code {
color: rgb(0 0 0 / var(--tw-text-opacity));
}
.checks .time-range > a:hover {
.targets .time-range > a:hover {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.checks .time-range > a:focus {
.targets .time-range > a:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
@ -1438,7 +1446,7 @@ code {
--tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity));
}
.checks .time-range > a.active {
.targets .time-range > a.active {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
@ -1446,7 +1454,7 @@ code {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.checks .time-range > a.active:hover {
.targets .time-range > a.active:hover {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}

View file

@ -0,0 +1,10 @@
/*!-----------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Version: 0.46.0(21007360cad28648bdf46282a2592cb47c3a7a6f)
* Released under the MIT license
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
*-----------------------------------------------------------------------------*/
define("vs/basic-languages/yaml/yaml", ["require","require"],(require)=>{
"use strict";var moduleExports=(()=>{var m=Object.create;var l=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,f=Object.prototype.hasOwnProperty;var w=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(n,t)=>(typeof require<"u"?require:n)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var S=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports),k=(e,n)=>{for(var t in n)l(e,t,{get:n[t],enumerable:!0})},a=(e,n,t,i)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of p(n))!f.call(e,r)&&r!==t&&l(e,r,{get:()=>n[r],enumerable:!(i=b(n,r))||i.enumerable});return e},c=(e,n,t)=>(a(e,n,"default"),t&&a(t,n,"default")),u=(e,n,t)=>(t=e!=null?m(g(e)):{},a(n||!e||!e.__esModule?l(t,"default",{value:e,enumerable:!0}):t,e)),y=e=>a(l({},"__esModule",{value:!0}),e);var d=S((C,s)=>{var h=u(w("vs/editor/editor.api"));s.exports=h});var $={};k($,{conf:()=>N,language:()=>x});var o={};c(o,u(d()));var N={comments:{lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{offSide:!0},onEnterRules:[{beforeText:/:\s*$/,action:{indentAction:o.languages.IndentAction.Indent}}]},x={tokenPostfix:".yaml",brackets:[{token:"delimiter.bracket",open:"{",close:"}"},{token:"delimiter.square",open:"[",close:"]"}],keywords:["true","True","TRUE","false","False","FALSE","null","Null","Null","~"],numberInteger:/(?:0|[+-]?[0-9]+)/,numberFloat:/(?:0|[+-]?[0-9]+)(?:\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/,numberOctal:/0o[0-7]+/,numberHex:/0x[0-9a-fA-F]+/,numberInfinity:/[+-]?\.(?:inf|Inf|INF)/,numberNaN:/\.(?:nan|Nan|NAN)/,numberDate:/\d{4}-\d\d-\d\d([Tt ]\d\d:\d\d:\d\d(\.\d+)?(( ?[+-]\d\d?(:\d\d)?)|Z)?)?/,escapes:/\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,tokenizer:{root:[{include:"@whitespace"},{include:"@comment"},[/%[^ ]+.*$/,"meta.directive"],[/---/,"operators.directivesEnd"],[/\.{3}/,"operators.documentEnd"],[/[-?:](?= )/,"operators"],{include:"@anchor"},{include:"@tagHandle"},{include:"@flowCollections"},{include:"@blockStyle"},[/@numberInteger(?![ \t]*\S+)/,"number"],[/@numberFloat(?![ \t]*\S+)/,"number.float"],[/@numberOctal(?![ \t]*\S+)/,"number.octal"],[/@numberHex(?![ \t]*\S+)/,"number.hex"],[/@numberInfinity(?![ \t]*\S+)/,"number.infinity"],[/@numberNaN(?![ \t]*\S+)/,"number.nan"],[/@numberDate(?![ \t]*\S+)/,"number.date"],[/(".*?"|'.*?'|[^#'"]*?)([ \t]*)(:)( |$)/,["type","white","operators","white"]],{include:"@flowScalars"},[/.+?(?=(\s+#|$))/,{cases:{"@keywords":"keyword","@default":"string"}}]],object:[{include:"@whitespace"},{include:"@comment"},[/\}/,"@brackets","@pop"],[/,/,"delimiter.comma"],[/:(?= )/,"operators"],[/(?:".*?"|'.*?'|[^,\{\[]+?)(?=: )/,"type"],{include:"@flowCollections"},{include:"@flowScalars"},{include:"@tagHandle"},{include:"@anchor"},{include:"@flowNumber"},[/[^\},]+/,{cases:{"@keywords":"keyword","@default":"string"}}]],array:[{include:"@whitespace"},{include:"@comment"},[/\]/,"@brackets","@pop"],[/,/,"delimiter.comma"],{include:"@flowCollections"},{include:"@flowScalars"},{include:"@tagHandle"},{include:"@anchor"},{include:"@flowNumber"},[/[^\],]+/,{cases:{"@keywords":"keyword","@default":"string"}}]],multiString:[[/^( +).+$/,"string","@multiStringContinued.$1"]],multiStringContinued:[[/^( *).+$/,{cases:{"$1==$S2":"string","@default":{token:"@rematch",next:"@popall"}}}]],whitespace:[[/[ \t\r\n]+/,"white"]],comment:[[/#.*$/,"comment"]],flowCollections:[[/\[/,"@brackets","@array"],[/\{/,"@brackets","@object"]],flowScalars:[[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/'[^']*'/,"string"],[/"/,"string","@doubleQuotedString"]],doubleQuotedString:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],blockStyle:[[/[>|][0-9]*[+-]?$/,"operators","@multiString"]],flowNumber:[[/@numberInteger(?=[ \t]*[,\]\}])/,"number"],[/@numberFloat(?=[ \t]*[,\]\}])/,"number.float"],[/@numberOctal(?=[ \t]*[,\]\}])/,"number.octal"],[/@numberHex(?=[ \t]*[,\]\}])/,"number.hex"],[/@numberInfinity(?=[ \t]*[,\]\}])/,"number.infinity"],[/@numberNaN(?=[ \t]*[,\]\}])/,"number.nan"],[/@numberDate(?=[ \t]*[,\]\}])/,"number.date"]],tagHandle:[[/\![^ ]*/,"tag"]],anchor:[[/[&*][^ ]+/,"namespace"]]}};return y($);})();
return moduleExports;
});

View file

@ -1,26 +1,26 @@
{{ define "main" }}
<div class="container max-w-screen-md flex flex-col mt-20 gap-20">
{{ $length := len .Checks }}
{{ $length := len .Targets }}
{{ if eq $length 0 }}
<section>
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1
class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl"
>
There are no checks yet.
There are no targets yet.
</h1>
<p
class="mb-8 text-l font-normal text-gray-700 lg:text-l sm:px-8 lg:px-40"
>
Create a check to check your services and get notified when they are
down.
Create a target to target your services and get notified when they
are down.
</p>
<div class="flex flex-col gap-4 sm:flex-row sm:justify-center">
<a
href="/settings/checks/create"
href="/settings/targets/create"
class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"
>
Create First Check
Create First Target
<svg class="feather ml-1 h-5 w-5 overflow-visible">
<use href="/static/icons/feather-sprite.svg#plus" />
</svg>
@ -69,7 +69,7 @@
</p>
</div>
{{ end }}
<div class="checks flex flex-col gap-4">
<div class="targets flex flex-col gap-4">
<div
class="inline-flex gap-1 justify-center md:justify-end time-range"
role="group"
@ -93,7 +93,7 @@
>90 Minutes</a
>
</div>
{{ range $group, $checksAndStatus := .Checks }}
{{ range $group, $targetsAndStatus := .Targets }}
<details
open
class="bg-white shadow-md rounded-lg p-6 py-4 gap-2 [&_svg]:open:rotate-90"
@ -101,11 +101,11 @@
<summary
class="flex flex-row gap-2 p-3 py-2 -mx-3 cursor-pointer hover:bg-blue-50 rounded-lg"
>
{{ if eq $checksAndStatus.Status "SUCCESS" }}
{{ if eq $targetsAndStatus.Status "SUCCESS" }}
<span
class="flex w-3 h-3 bg-green-400 rounded-full self-center"
></span>
{{ else if eq $checksAndStatus.Status "FAILURE" }}
{{ else if eq $targetsAndStatus.Status "FAILURE" }}
<span
class="flex w-3 h-3 bg-red-400 rounded-full self-center"
></span>
@ -123,7 +123,7 @@
<use href="/static/icons/feather-sprite.svg#chevron-right" />
</svg>
</summary>
{{ range $checksAndStatus.Checks }}
{{ range $targetsAndStatus.Targets }}
<div
class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2 pb-2 border-b last-of-type:pb-0 last-of-type:border-0 border-gray-100"
>

View file

@ -48,9 +48,8 @@
</caption>
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col">Check Group</th>
<th scope="col">Name</th>
<th scope="col">Visibility</th>
<th scope="col">Targets</th>
<th scope="col">Worker Groups</th>
<th scope="col">State</th>
<th scope="col">Schedule</th>
@ -58,90 +57,54 @@
</tr>
</thead>
<tbody>
{{ range .CheckGroups }}
{{ $currentGroup := . }}
<tr class="row-special">
<th scope="rowgroup">
{{ . }}
{{ range $checks := .Checks }}
<tr>
<th scope="row">
{{ .Name }}
</th>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{{ range $group, $checks := $.Checks }}
{{ if eq $group $currentGroup }}
{{ range $checks }}
<tr>
<th scope="row" aria-hidden="true">└─</th>
<th scope="row">
{{ .Name }}
</th>
<td>
{{ if eq .Visibility "PUBLIC" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
Public
</span>
{{ else if eq .Visibility "PRIVATE" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-fuchsia-100 text-fuchsia-800"
>
Private
</span>
{{ else }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
Unknown
</span>
{{ end }}
</td>
<td>
{{ range .WorkerGroups }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ . }}
</span>
{{ end }}
</td>
<td>
{{ if eq .State "ACTIVE" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>
ACTIVE
</span>
{{ else if eq .State "PAUSED" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
>
PAUSED
</span>
{{ else }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
UNKNOWN
</span>
{{ end }}
</td>
<td>
{{ .Schedule }}
</td>
<td>
<a href="/settings/checks/{{ .Id }}" class="link"
>Details</a
>
</td>
</tr>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>3</span
>
</td>
<td>
{{ range .WorkerGroups }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ . }}
</span>
{{ end }}
{{ end }}
{{ end }}
</td>
<td>
{{ if eq .State "ACTIVE" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>
ACTIVE
</span>
{{ else if eq .State "PAUSED" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
>
PAUSED
</span>
{{ else }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
UNKNOWN
</span>
{{ end }}
</td>
<td>
{{ .Schedule }}
</td>
<td>
<a href="/settings/checks/{{ .Id }}" class="link">Details</a>
</td>
</tr>
{{ end }}
</tbody>
</table>

View file

@ -2,40 +2,20 @@
<section class="p-5">
<form action="/settings/checks/create" method="post">
<label for="name">Name</label>
<input type="text" name="name" id="name" placeholder="Github.com" />
<input type="text" name="name" id="name" placeholder="HTTP GET Request" />
<p>Name of the check can be anything.</p>
<label for="visibility">Visibility</label>
<select name="visibility" id="visibility" required>
<option value="PUBLIC">Public</option>
<option value="PRIVATE">Private</option>
</select>
<p>
Visibility determines who can see the check. If set to
<code>public</code>, it will be visible to everyone on the homepage.
Otherwise it will be only visible to signed in users.
</p>
<label for="group">Check Group</label>
<input
type="text"
name="group"
id="group"
placeholder="default"
value="default"
required
/>
<p>
Group checks together. This affects how they are presented on the
homepage.
</p>
<label for="workergroups">Worker Groups</label>
<input
type="text"
name="workergroups"
id="workergroups"
placeholder="NA EU"
placeholder="europe asia"
required
/>
<p>Worker groups are used to distribute the check to specific workers.</p>
<p>
Worker groups are used to distribute the check to specific workers.
Space is a separator between groups.
</p>
<label for="schedule">Schedule</label>
<input
type="text"
@ -54,12 +34,30 @@
<code>@daily</code>, <code>@weekly</code>, <code>@monthly</code>,
<code>@yearly</code>.
</p>
<label for="script">Script</label>
<textarea required id="script" name="script" class="h-96">
{{ ScriptUnescapeString .Example }}</textarea
<label for="filter">Filter</label>
<textarea required id="filter" name="filter" class="h-12">
{{ ScriptUnescapeString .ExampleFilter }}</textarea
>
<div
id="editor"
id="editor-filter"
class="hidden block w-full h-12 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p>
With filter we specify what targets the check will run on. For whole
grammar on what the filter query can look like, please read the
<a
target="_blank"
href="https://github.com/hashicorp/mql/blob/main/GRAMMAR.md"
>MQL Grammar</a
>
documentation.
</p>
<label for="script">Script</label>
<textarea required id="script" name="script" class="h-96">
{{ ScriptUnescapeString .ExampleScript }}</textarea
>
<div
id="editor-script"
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p>
@ -75,36 +73,42 @@
<script src="/static/monaco/vs/loader.js"></script>
<script>
function htmlDecode(input) {
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
script = htmlDecode("{{ .Example }}");
document.getElementById("editor").classList.remove("hidden");
document.getElementById("script").hidden = true;
const items = [
{ name: "filter", language: "" },
{ name: "script", language: "javascript" },
];
function save() {
const script = window.editor.getValue();
document.getElementById("script").value = script;
for (const { name } of items) {
const elem = window.editors[name].getValue();
document.getElementById(name).value = elem;
}
}
require.config({ paths: { vs: "/static/monaco/vs" } });
require(["vs/editor/editor.main"], function () {
window.editor = monaco.editor.create(document.getElementById("editor"), {
value: script,
language: "javascript",
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
window.editors = {};
for (const { name, language } of items) {
const textarea = document.getElementById(name);
const editor = document.getElementById("editor-" + name);
const divElem = document.getElementById("editor");
const resizeObserver = new ResizeObserver((entries) => {
window.editor.layout();
editor.classList.remove("hidden");
textarea.hidden = true;
require.config({ paths: { vs: "/static/monaco/vs" } });
require(["vs/editor/editor.main"], function () {
window.editors[name] = monaco.editor.create(editor, {
value: textarea.value,
language: language,
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(editor);
});
resizeObserver.observe(divElem);
});
}
</script>
{{ end }}

View file

@ -2,38 +2,6 @@
<section class="p-5">
<form action="/settings/checks/{{ .Check.Id }}" method="post">
<h2>Configuration</h2>
<label for="visibility">Visibility</label>
<select name="visibility" id="visibility" required>
<option
{{ if eq .Check.Visibility "PUBLIC" }}selected="selected"{{ end }}
value="PUBLIC"
>
Public
</option>
<option
{{ if eq .Check.Visibility "PRIVATE" }}selected="selected"{{ end }}
value="PRIVATE"
>
Private
</option>
</select>
<p>
Visibility determines who can see the check. If set to
<code>public</code>, it will be visible to everyone on the homepage.
Otherwise it will be only visible to signed in users.
</p>
<label for="group">Check Group</label>
<input
type="text"
name="group"
id="group"
value="{{ .Check.Group }}"
required
/>
<p>
Group checks together. This affects how they are presented on the
homepage.
</p>
<label for="workergroups">Worker Groups</label>
<input
type="text"
@ -60,12 +28,30 @@
<code>@daily</code>, <code>@weekly</code>, <code>@monthly</code>,
<code>@yearly</code>.
</p>
<label for="filter">Filter</label>
<textarea required id="filter" name="filter" class="h-12">
{{ ScriptUnescapeString .Check.Filter }}</textarea
>
<div
id="editor-filter"
class="hidden block w-full h-12 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p>
With filter we specify what targets the check will run on. For whole
grammar on what the filter query can look like, please read the
<a
target="_blank"
href="https://github.com/hashicorp/mql/blob/main/GRAMMAR.md"
>MQL Grammar</a
>
documentation.
</p>
<label for="script">Script</label>
<textarea required id="script" name="script" class="h-96">
{{ ScriptUnescapeString .Check.Script }}</textarea
>
<div
id="editor"
id="editor-script"
class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden hidden"
></div>
<p>
@ -135,6 +121,7 @@
</caption>
<thead>
<tr>
<th>Check ID</th>
<th>Status</th>
<th>Worker Group</th>
<th>Created At</th>
@ -144,70 +131,100 @@
</thead>
<tbody>
{{ range .History }}
<tr>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }}
bg-green-100 text-green-800
{{ else }}
bg-red-100 text-red-800
{{ end }}"
>
{{ .Status }}
</span>
</td>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .WorkerGroupName }}
</span>
</td>
<td>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
</td>
<td>{ .Duration }</td>
<td class="whitespace-normal">
{{ .Note }}
</td>
</tr>
{{ if eq .Status "Running" }}
<tr>
<td>{{ .CheckId }}</td>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }}
bg-green-100 text-green-800
{{ else }}
bg-red-100 text-red-800
{{ end }}"
>
{{ .Status }}
</span>
</td>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{ .WorkerGroupName }
</span>
</td>
<td>{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }</td>
<td>/</td>
<td class="whitespace-normal">/</td>
</tr>
{{ else }}
<tr>
<td>{{ .CheckId }}</td>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }}
bg-green-100 text-green-800
{{ else }}
bg-red-100 text-red-800
{{ end }}"
>
{{ .Status }}
</span>
</td>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{ .WorkerGroupName }
</span>
</td>
<td>{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }</td>
<td>{{ .Duration }}</td>
<td class="whitespace-normal">{ .Note }</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
</section>
<script src="/static/monaco/vs/loader.js"></script>
<script>
document.getElementById("editor").classList.remove("hidden");
document.getElementById("script").hidden = true;
<script>
const items = [
{ name: "filter", language: "" },
{ name: "script", language: "javascript" },
];
function save() {
const script = window.editor.getValue();
document.getElementById('script').value = script;
}
function save() {
for (const { name } of items) {
const elem = window.editors[name].getValue();
document.getElementById(name).value = elem;
}
}
function htmlDecode(input) {
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
script = htmlDecode("{{ .Check.Script }}")
window.editors = {};
for (const { name, language } of items) {
const textarea = document.getElementById(name);
const editor = document.getElementById("editor-" + name);
require.config({ paths: { vs: '/static/monaco/vs' } });
require(['vs/editor/editor.main'], function () {
window.editor = monaco.editor.create(document.getElementById('editor'), {
value: script,
language: 'javascript',
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
editor.classList.remove("hidden");
textarea.hidden = true;
const divElem = document.getElementById('editor');
const resizeObserver = new ResizeObserver(entries => {
window.editor.layout();
});
resizeObserver.observe(divElem);
});
</script>
require.config({ paths: { vs: "/static/monaco/vs" } });
require(["vs/editor/editor.main"], function () {
window.editors[name] = monaco.editor.create(editor, {
value: textarea.value,
language: language,
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(editor);
});
}
</script>
{{ end }}

View file

@ -21,9 +21,7 @@
<div
class="inline-block bg-white rounded-lg shadow p-5 text-center sm:text-left"
>
<h3 class="text-sm leading-6 font-medium text-gray-400">
Total Checks
</h3>
<h3 class="text-sm leading-6 font-medium text-gray-400">Total Checks</h3>
<p class="text-3xl font-bold text-black">{{ .ChecksCount }}</p>
</div>
<div
@ -66,14 +64,14 @@
<a
class="underline hover:text-blue-600"
href="/settings/checks/{{ .CheckId }}"
>{{ .CheckName }}</a
>{{ .CheckId }}</a
>
</th>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .WorkerGroupName }}
{ .WorkerGroupName }
</span>
</td>
<td>
@ -87,12 +85,8 @@
{{ .Status }}
</span>
</td>
<td>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
</td>
<td class="whitespace-normal">
{{ .Note }}
</td>
<td>{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }</td>
<td class="whitespace-normal">{ .Note }</td>
</tr>
{{ end }}
</tbody>

View file

@ -55,32 +55,88 @@
</a>
</div>
</caption>
<thead>
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th>Name</th>
<th>Type</th>
<th>Action</th>
<th scope="col">Target Group</th>
<th scope="col">Name</th>
<th scope="col">Visibility</th>
<th scope="col">State</th>
<th scope="col">Action</th>
</tr>
</thead>
{{ range .Targets }}
<tbody>
<tr>
<th scope="row">
{{ .Name }}
<tbody>
{{ range .TargetGroups }}
{{ $currentGroup := . }}
<tr class="row-special">
<th scope="rowgroup">
{{ . }}
</th>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .Type }}
</span>
</td>
<td>
<a href="/settings/targets/{{ .Id }}" class="link">Details</a>
</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
{{ end }}
{{ range $group, $targets := $.Targets }}
{{ if eq $group $currentGroup }}
{{ range $targets }}
<tr>
<th scope="row" aria-hidden="true">└─</th>
<th scope="row">
{{ .Name }}
</th>
<td>
{{ if eq .Visibility "PUBLIC" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
Public
</span>
{{ else if eq .Visibility "PRIVATE" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-fuchsia-100 text-fuchsia-800"
>
Private
</span>
{{ else }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
Unknown
</span>
{{ end }}
</td>
<td>
{{ if eq .State "ACTIVE" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>
ACTIVE
</span>
{{ else if eq .State "PAUSED" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
>
PAUSED
</span>
{{ else }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
UNKNOWN
</span>
{{ end }}
</td>
<td>
<a href="/settings/targets/{{ .Id }}" class="link"
>Details</a
>
</td>
</tr>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
</tbody>
</table>
</section>
{{ end }}

View file

@ -0,0 +1,86 @@
{{ define "settings" }}
<section class="p-5">
<form action="/settings/targets/create" method="post">
<label for="name">Name</label>
<input type="text" name="name" id="name" placeholder="Github.com" />
<p>Name of the target can be anything.</p>
<label for="visibility">Visibility</label>
<select name="visibility" id="visibility" required>
<option value="PUBLIC">Public</option>
<option value="PRIVATE">Private</option>
</select>
<p>
Visibility determines who can see the target. If set to
<code>public</code>, it will be visible to everyone on the homepage.
Otherwise it will be only visible to signed in users.
</p>
<label for="group">Target Group</label>
<input
type="text"
name="group"
id="group"
placeholder="default"
value="default"
required
/>
<p>
Group targets together. This affects how they are presented on the
homepage.
</p>
<label for="metadata">Metadata</label>
<textarea required id="metadata" name="metadata" class="h-96">
{{ ScriptUnescapeString .Example }}</textarea
>
<div
id="editor-metadata"
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p>
Metadata is a YAML object that contains the configuration for the
target. This configuration can be then used for <code>Checks</code> to
filter the targets to act on as well as by using
<code>getTarget()</code>
function to fetch target metadata.
</p>
<button type="submit" onclick="save()">Create</button>
</form>
</section>
<script src="/static/monaco/vs/loader.js"></script>
<script>
const items = [{ name: "metadata", language: "yaml" }];
function save() {
for (const { name } of items) {
const elem = window.editors[name].getValue();
document.getElementById(name).value = elem;
}
}
window.editors = {};
for (const { name, language } of items) {
const textarea = document.getElementById(name);
const editor = document.getElementById("editor-" + name);
editor.classList.remove("hidden");
textarea.hidden = true;
require.config({ paths: { vs: "/static/monaco/vs" } });
require(["vs/editor/editor.main"], function () {
window.editors[name] = monaco.editor.create(editor, {
value: textarea.value,
language: language,
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(editor);
});
}
</script>
{{ end }}

View file

@ -0,0 +1,190 @@
{{ define "settings" }}
<section class="p-5">
<form action="/settings/targets/{{ .Target.Id }}" method="post">
<h2>Configuration</h2>
<label for="visibility">Visibility</label>
<select name="visibility" id="visibility" required>
<option
{{ if eq .Target.Visibility "PUBLIC" }}selected="selected"{{ end }}
value="PUBLIC"
>
Public
</option>
<option
{{ if eq .Target.Visibility "PRIVATE" }}selected="selected"{{ end }}
value="PRIVATE"
>
Private
</option>
</select>
<p>
Visibility determines who can see the target. If set to
<code>public</code>, it will be visible to everyone on the homepage.
Otherwise it will be only visible to signed in users.
</p>
<label for="group">Target Group</label>
<input
type="text"
name="group"
id="group"
value="{{ .Target.Group }}"
required
/>
<p>
Group targets together. This affects how they are presented on the
homepage.
</p>
<label for="metadata">Metadata</label>
<textarea required id="metadata" name="metadata" class="h-96">
{{ ScriptUnescapeString .Target.Metadata }}</textarea
>
<div
id="editor-metadata"
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p>
Metadata is a YAML object that contains the configuration for the
target. This configuration can be then used for <code>Targets</code> to
filter the targets to act on as well as by using
<code>getTarget()</code>
function to fetch target metadata.
</p>
<button type="submit" onclick="save()">Save</button>
</form>
</section>
<div class="flex md:flex-row flex-col gap-4 h-min">
<section class="p-5 flex-1">
<h2 class="mb-2 flex flex-row gap-2">
State
{{ if eq .Target.State "ACTIVE" }}
<span
class="self-center h-fit w-fit px-2 text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>
ACTIVE
</span>
{{ else if eq .Target.State "PAUSED" }}
<span
class="self-center h-fit w-fit px-2 text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
>
PAUSED
</span>
{{ end }}
</h2>
<p class="text-sm mb-2">
Pausing the target will stop it from executing. This can be useful in
cases of expected downtime. Or when the target is not needed anymore.
</p>
{{ if eq .Target.State "ACTIVE" }}
<a
class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100"
href="/settings/targets/{{ .Target.Id }}/disable"
>Pause</a
>
{{ else if eq .Target.State "PAUSED" }}
<a
class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100"
href="/settings/targets/{{ .Target.Id }}/enable"
>Resume</a
>
{{ end }}
</section>
<section class="p-2 flex-1 border-4 border-red-300">
<h2 class="mb-2">Danger Zone</h2>
<p class="text-sm mb-2">Permanently delete this target.</p>
<a
class="block text-center focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2"
href="/settings/targets/{{ .Target.Id }}/delete"
>Delete</a
>
</section>
</div>
<section>
<table>
<caption>
History
<p>Last 10 executions of the targets.</p>
</caption>
<thead>
<tr>
<th>Status</th>
<th>Worker Group</th>
<th>Created At</th>
<th>Duration</th>
<th>Note</th>
</tr>
</thead>
<tbody>
{{ range .History }}
<tr>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }}
bg-green-100 text-green-800
{{ else }}
bg-red-100 text-red-800
{{ end }}"
>
{{ .Status }}
</span>
</td>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .WorkerGroupName }}
</span>
</td>
<td>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
</td>
<td>{ .Duration }</td>
<td class="whitespace-normal">
{{ .Note }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</section>
<script src="/static/monaco/vs/loader.js"></script>
<script>
const items = [{ name: "metadata", language: "yaml" }];
function save() {
for (const { name } of items) {
const elem = window.editors[name].getValue();
document.getElementById(name).value = elem;
}
}
window.editors = {};
for (const { name, language } of items) {
const textarea = document.getElementById(name);
const editor = document.getElementById("editor-" + name);
editor.classList.remove("hidden");
textarea.hidden = true;
require.config({ paths: { vs: "/static/monaco/vs" } });
require(["vs/editor/editor.main"], function () {
window.editors[name] = monaco.editor.create(editor, {
value: textarea.value,
language: language,
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(editor);
});
}
</script>
{{ end }}

View file

@ -14,7 +14,7 @@
{{ ScriptUnescapeString .Example }}</textarea
>
<div
id="editor"
id="editor-script"
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p>
@ -30,36 +30,39 @@
<script src="/static/monaco/vs/loader.js"></script>
<script>
function htmlDecode(input) {
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
script = htmlDecode("{{ .Example }}");
document.getElementById("editor").classList.remove("hidden");
document.getElementById("script").hidden = true;
const items = [{ name: "script", language: "javascript" }];
function save() {
const script = window.editor.getValue();
document.getElementById("script").value = script;
for (const { name } of items) {
const elem = window.editors[name].getValue();
document.getElementById(name).value = elem;
}
}
require.config({ paths: { vs: "/static/monaco/vs" } });
require(["vs/editor/editor.main"], function () {
window.editor = monaco.editor.create(document.getElementById("editor"), {
value: script,
language: "javascript",
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
window.editors = {};
for (const { name, language } of items) {
const textarea = document.getElementById(name);
const editor = document.getElementById("editor-" + name);
const divElem = document.getElementById("editor");
const resizeObserver = new ResizeObserver((entries) => {
window.editor.layout();
editor.classList.remove("hidden");
textarea.hidden = true;
require.config({ paths: { vs: "/static/monaco/vs" } });
require(["vs/editor/editor.main"], function () {
window.editors[name] = monaco.editor.create(editor, {
value: textarea.value,
language: language,
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(editor);
});
resizeObserver.observe(divElem);
});
}
</script>
{{ end }}

View file

@ -7,7 +7,7 @@
{{ ScriptUnescapeString .Trigger.Script }}</textarea
>
<div
id="editor"
id="editor-script"
class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden hidden"
></div>
<p>
@ -111,37 +111,40 @@
</section>
<script src="/static/monaco/vs/loader.js"></script>
<script>
document.getElementById("editor").classList.remove("hidden");
document.getElementById("script").hidden = true;
<script>
const items = [{ name: "script", language: "javascript" }];
function save() {
const script = window.editor.getValue();
document.getElementById('script').value = script;
}
function save() {
for (const { name } of items) {
const elem = window.editors[name].getValue();
document.getElementById(name).value = elem;
}
}
function htmlDecode(input) {
var doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
script = htmlDecode("{{ .Trigger.Script }}")
window.editors = {};
for (const { name, language } of items) {
const textarea = document.getElementById(name);
const editor = document.getElementById("editor-" + name);
require.config({ paths: { vs: '/static/monaco/vs' } });
require(['vs/editor/editor.main'], function () {
window.editor = monaco.editor.create(document.getElementById('editor'), {
value: script,
language: 'javascript',
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
editor.classList.remove("hidden");
textarea.hidden = true;
const divElem = document.getElementById('editor');
const resizeObserver = new ResizeObserver(entries => {
window.editor.layout();
});
resizeObserver.observe(divElem);
});
</script>
require.config({ paths: { vs: "/static/monaco/vs" } });
require(["vs/editor/editor.main"], function () {
window.editors[name] = monaco.editor.create(editor, {
value: textarea.value,
language: language,
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(editor);
});
}
</script>
{{ end }}

View file

@ -8,8 +8,8 @@ import (
"text/template"
"time"
"code.tjo.space/mentos1386/zdravko/internal/script"
"github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/pkg/script"
)
//go:embed *
@ -51,6 +51,8 @@ func NewTemplates() *Templates {
"settings_triggers_create.tmpl": loadSettings("pages/settings_triggers_create.tmpl"),
"settings_triggers_describe.tmpl": loadSettings("pages/settings_triggers_describe.tmpl"),
"settings_targets.tmpl": loadSettings("pages/settings_targets.tmpl"),
"settings_targets_create.tmpl": loadSettings("pages/settings_targets_create.tmpl"),
"settings_targets_describe.tmpl": loadSettings("pages/settings_targets_describe.tmpl"),
"settings_incidents.tmpl": loadSettings("pages/settings_incidents.tmpl"),
"settings_notifications.tmpl": loadSettings("pages/settings_notifications.tmpl"),
"settings_worker_groups.tmpl": loadSettings("pages/settings_worker_groups.tmpl"),