Merge pull request #5 from mentos1386/target-instead-of-checks

Target instead of checks
This commit is contained in:
Tine Jozelj 2024-05-25 19:52:50 +02:00 committed by GitHub
commit 91fddaafaa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
83 changed files with 2225 additions and 1057 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"`
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,49 @@ 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"`
TargetId string `db:"target_id"`
WorkerGroupId string `db:"worker_group_id"`
CheckId string `db:"check_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,48 @@ 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,
worker_group_id TEXT NOT NULL,
check_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, worker_group_id, check_id, created_at),
CONSTRAINT fk_target_histories_target FOREIGN KEY (target_id) REFERENCES targets(id) ON DELETE CASCADE,
CONSTRAINT fk_target_histories_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE,
CONSTRAINT fk_target_histories_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE
) STRICT;
-- +migrate Down
@ -105,9 +112,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

@ -17,9 +17,9 @@ primary_region = 'waw'
ROOT_URL = 'https://zdravko.mnts.dev'
TEMPORAL_SERVER_HOST = 'server.process.zdravko.internal:7233'
TEMPORAL_DATABASE_PATH = '/data/temporal-10.db'
SQLITE_DATABASE_PATH = '/data/zdravko-10.db'
KEYVALUE_DATABASE_PATH = '/data/zdravko_kv-10.db'
TEMPORAL_DATABASE_PATH = '/data/temporal-11.db'
SQLITE_DATABASE_PATH = '/data/zdravko-11.db'
KEYVALUE_DATABASE_PATH = '/data/zdravko_kv-11.db'
[processes]
server = '--temporal --server'

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.

20
go.mod
View file

@ -1,9 +1,10 @@
module code.tjo.space/mentos1386/zdravko
module github.com/mentos1386/zdravko
go 1.21.6
require (
github.com/dgraph-io/badger/v4 v4.2.0
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
github.com/go-playground/validator/v10 v10.18.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/gorilla/sessions v1.2.2
@ -20,7 +21,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
)
@ -56,7 +57,6 @@ require (
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.9.0 // indirect
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
@ -177,14 +177,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

32
go.sum
View file

@ -569,16 +569,16 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
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=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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=
@ -594,8 +594,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
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=
@ -618,8 +618,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
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=
@ -635,8 +635,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
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=
@ -668,8 +668,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
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=
@ -684,8 +684,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
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=
@ -706,8 +706,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
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 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
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,30 @@
package activities
import (
"context"
"github.com/mentos1386/zdravko/database/models"
"github.com/mentos1386/zdravko/internal/server/services"
"github.com/mentos1386/zdravko/internal/temporal"
)
func (a *Activities) AddTargetHistory(ctx context.Context, param temporal.ActivityAddTargetHistoryParam) (*temporal.ActivityAddTargetHistoryResult, error) {
status := models.TargetStatusUnknown
if param.Status == temporal.AddTargetHistoryStatusSuccess {
status = models.TargetStatusSuccess
}
if param.Status == temporal.AddTargetHistoryStatusFailure {
status = models.TargetStatusFailure
}
err := services.AddHistoryForTarget(ctx, a.db, &models.TargetHistory{
TargetId: param.Target.Id,
WorkerGroupId: param.WorkerGroupId,
CheckId: param.CheckId,
Status: status,
Note: param.Note,
})
return &temporal.ActivityAddTargetHistoryResult{}, err
}

View file

@ -0,0 +1,73 @@
package activities
import (
"context"
"github.com/dop251/goja"
"github.com/mentos1386/zdravko/internal/server/services"
"github.com/mentos1386/zdravko/internal/temporal"
"github.com/mentos1386/zdravko/pkg/script"
"gopkg.in/yaml.v3"
)
func (a *Activities) TargetsFilter(ctx context.Context, param temporal.ActivityTargetsFilterParam) (*temporal.ActivityTargetsFilterResult, error) {
a.logger.Info("TargetsFilter", "filter", param.Filter)
allTargets, err := services.GetTargets(ctx, a.db)
if err != nil {
return nil, err
}
filteredTargets := make([]*temporal.Target, 0)
program, err := goja.Compile("filter", script.UnescapeString(param.Filter), false)
if err != nil {
return nil, err
}
for _, target := range allTargets {
vm := goja.New()
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())
var metadata map[string]interface{}
err := yaml.Unmarshal([]byte(target.Metadata), &metadata)
if err != nil {
return nil, err
}
a.logger.Info("TargetsFilter", "target", target)
targetWithMedatada := &struct {
Id string
Name string
Group string
Metadata map[string]interface{}
}{
Id: target.Id,
Name: target.Name,
Group: target.Group,
Metadata: metadata,
}
err = vm.Set("target", targetWithMedatada)
if err != nil {
return nil, err
}
value, err := vm.RunProgram(program)
if err != nil {
return nil, err
}
if value.Export().(bool) {
filteredTargets = append(filteredTargets, &temporal.Target{
Id: target.Id,
Name: target.Name,
Group: target.Group,
Metadata: target.Metadata,
})
}
}
return &temporal.ActivityTargetsFilterResult{
Targets: filteredTargets,
}, 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,11 +7,11 @@ 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"
const authenticationSessionName = "zdravko-hey"
type AuthenticatedPrincipal struct {
User *AuthenticatedUser
@ -48,7 +48,7 @@ func GetUser(ctx context.Context) *AuthenticatedUser {
}
func (h *BaseHandler) AuthenticateRequestWithCookies(r *http.Request) (*AuthenticatedUser, error) {
session, err := h.store.Get(r, sessionName)
session, err := h.store.Get(r, authenticationSessionName)
if err != nil {
return nil, err
}
@ -114,7 +114,7 @@ func (h *BaseHandler) AuthenticateRequestWithToken(r *http.Request) (*Authentica
}
func (h *BaseHandler) SetAuthenticatedUserForRequest(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) error {
session, err := h.store.Get(r, sessionName)
session, err := h.store.Get(r, authenticationSessionName)
if err != nil {
return err
}
@ -124,24 +124,16 @@ func (h *BaseHandler) SetAuthenticatedUserForRequest(w http.ResponseWriter, r *h
session.Values["oauth2_refresh_token"] = user.OAuth2RefreshToken
session.Values["oauth2_token_type"] = user.OAuth2TokenType
session.Values["oauth2_expiry"] = user.OAuth2Expiry.Format(time.RFC3339)
err = h.store.Save(r, w, session)
if err != nil {
return err
}
return nil
return h.store.Save(r, w, session)
}
func (h *BaseHandler) ClearAuthenticatedUserForRequest(w http.ResponseWriter, r *http.Request) error {
session, err := h.store.Get(r, sessionName)
session, err := h.store.Get(r, authenticationSessionName)
if err != nil {
return err
}
session.Options.MaxAge = -1
err = h.store.Save(r, w, session)
if err != nil {
return err
}
return nil
return h.store.Save(r, w, session)
}
type AuthenticatedHandler func(http.ResponseWriter, *http.Request, *AuthenticatedPrincipal)
@ -159,7 +151,7 @@ func (h *BaseHandler) Authenticated(next echo.HandlerFunc) echo.HandlerFunc {
if user.OAuth2Expiry.Before(time.Now()) {
user, err = h.RefreshToken(c.Response(), c.Request(), user)
if err != nil {
return c.Redirect(http.StatusTemporaryRedirect, "/oauth2/login")
return c.Redirect(http.StatusTemporaryRedirect, "/oauth2/login?redirect="+c.Request().URL.Path)
}
}
@ -173,6 +165,6 @@ func (h *BaseHandler) Authenticated(next echo.HandlerFunc) echo.HandlerFunc {
return next(cc)
}
return c.Redirect(http.StatusTemporaryRedirect, "/oauth2/login")
return c.Redirect(http.StatusTemporaryRedirect, "/oauth2/login?redirect="+c.Request().URL.Path)
}
}

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: |
target.metadata.kind == "Http" && target.metadata.spec.url != ""
target: |
kind: Http
labels:
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"
)
@ -19,7 +19,9 @@ var examplesYaml embed.FS
type examples struct {
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
Targets map[string]TargetsAndStatus
TargetsLength int
TimeRange string
Status models.CheckStatus
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,13 +12,52 @@ 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"
)
const oauth2RedirectSessionName = "zdravko-hey-oauth2"
func (h *BaseHandler) setOAuth2Redirect(c echo.Context, redirect string) error {
w := c.Response()
r := c.Request()
session, err := h.store.Get(r, oauth2RedirectSessionName)
if err != nil {
return err
}
session.Values["redirect"] = redirect
return h.store.Save(r, w, session)
}
func (h *BaseHandler) getOAuth2Redirect(c echo.Context) (string, error) {
r := c.Request()
session, err := h.store.Get(r, oauth2RedirectSessionName)
if err != nil {
return "", err
}
if session.IsNew {
return "", nil
}
return session.Values["redirect"].(string), nil
}
func (h *BaseHandler) clearOAuth2Redirect(c echo.Context) error {
w := c.Response()
r := c.Request()
session, err := h.store.Get(r, oauth2RedirectSessionName)
if err != nil {
return err
}
session.Options.MaxAge = -1
return h.store.Save(r, w, session)
}
type UserInfo struct {
Id int `json:"id"` // FIXME: This might not always be int?
Sub string `json:"sub"`
@ -97,6 +136,14 @@ func (h *BaseHandler) OAuth2LoginGET(c echo.Context) error {
url := conf.AuthCodeURL(state, oauth2.AccessTypeOffline)
redirect := c.QueryParam("redirect")
h.logger.Info("OAuth2LoginGET", "redirect", redirect)
err = h.setOAuth2Redirect(c, redirect)
if err != nil {
return err
}
return c.Redirect(http.StatusTemporaryRedirect, url)
}
@ -156,7 +203,21 @@ func (h *BaseHandler) OAuth2CallbackGET(c echo.Context) error {
return err
}
return c.Redirect(http.StatusTemporaryRedirect, "/settings")
redirect, err := h.getOAuth2Redirect(c)
if err != nil {
return err
}
h.logger.Info("OAuth2CallbackGET", "redirect", redirect)
if redirect == "" {
redirect = "/settings"
}
err = h.clearOAuth2Redirect(c)
if err != nil {
return err
}
return c.Redirect(http.StatusTemporaryRedirect, redirect)
}
func (h *BaseHandler) OAuth2LogoutGET(c echo.Context) error {

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 {
@ -68,7 +71,8 @@ func (h *BaseHandler) SettingsChecksGET(c echo.Context) error {
for i, check := range checks {
state, err := services.GetCheckState(context.Background(), h.temporal, check.Id)
if err != nil {
return err
h.logger.Error("Failed to get check state", "error", err)
state = models.CheckStateUnknown
}
checksWithState[i] = &CheckWithWorkerGroupsAndState{
CheckWithWorkerGroups: check,
@ -76,23 +80,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 +110,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 +190,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 +204,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 +262,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 +273,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 {
@ -313,11 +305,10 @@ 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),
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 {

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,9 +265,9 @@ func CreateOrUpdateCheckSchedule(
},
Action: &client.ScheduleWorkflowAction{
ID: getScheduleId(check.Id),
Workflow: workflows.NewWorkflows(nil).CheckWorkflowDefinition,
Workflow: internaltemporal.WorkflowCheckName,
Args: args,
TaskQueue: "default",
TaskQueue: internaltemporal.TEMPORAL_SERVER_QUEUE,
RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 3,
},

View file

@ -0,0 +1,105 @@
package services
import (
"context"
"fmt"
"time"
"github.com/mentos1386/zdravko/internal/temporal"
"go.temporal.io/api/enums/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/client"
)
type CheckHistory struct {
CheckId string
Status string
Duration time.Duration
StartTime time.Time
EndTime time.Time
WorkerGroupName string
Note string
}
func GetLastNCheckHistory(ctx context.Context, t client.Client, n int32) ([]*CheckHistory, error) {
var checkHistory []*CheckHistory
response, err := t.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)
// Remove the quotes around the checkId and the prefix.
checkId := scheduleId[len("\"check-") : len(scheduleId)-1]
var result temporal.WorkflowCheckResult
if execution.Status != enums.WORKFLOW_EXECUTION_STATUS_RUNNING {
workflowRun := t.GetWorkflow(ctx, execution.GetExecution().GetWorkflowId(), execution.GetExecution().GetRunId())
err := workflowRun.Get(ctx, &result)
if err != nil {
return nil, err
}
}
checkHistory = append(checkHistory, &CheckHistory{
CheckId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
StartTime: execution.StartTime.AsTime(),
EndTime: execution.CloseTime.AsTime(),
Status: execution.Status.String(),
WorkerGroupName: execution.GetTaskQueue(),
Note: result.Note,
})
}
return checkHistory, nil
}
func GetCheckHistoryForCheck(ctx context.Context, t client.Client, checkId string) ([]*CheckHistory, error) {
var checkHistory []*CheckHistory
response, err := t.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)
// Remove the quotes around the checkId and the prefix.
checkId := scheduleId[len("\"check-") : len(scheduleId)-1]
var result temporal.WorkflowCheckResult
if execution.Status != enums.WORKFLOW_EXECUTION_STATUS_RUNNING {
workflowRun := t.GetWorkflow(ctx, execution.GetExecution().GetWorkflowId(), execution.GetExecution().GetRunId())
err := workflowRun.Get(ctx, &result)
if err != nil {
return nil, err
}
}
checkHistory = append(checkHistory, &CheckHistory{
CheckId: checkId,
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
StartTime: execution.StartTime.AsTime(),
EndTime: execution.CloseTime.AsTime(),
Status: execution.Status.String(),
WorkerGroupName: execution.GetTaskQueue(),
Note: result.Note,
})
}
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,73 @@
package services
import (
"context"
"github.com/jmoiron/sqlx"
"github.com/mentos1386/zdravko/database/models"
)
type TargetHistory struct {
*models.TargetHistory
TargetName string `db:"target_name"`
WorkerGroupName string `db:"worker_group_name"`
CheckName string `db:"check_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,
wg.name AS worker_group_name,
c.name AS check_name
FROM target_histories th
LEFT JOIN targets t ON th.target_id = t.id
LEFT JOIN worker_groups wg ON th.worker_group_id = wg.id
LEFT JOIN checks c ON th.check_id = c.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,
wg.name AS worker_group_name,
c.name AS check_name
FROM target_histories th
LEFT JOIN targets t ON th.target_id = t.id
LEFT JOIN worker_groups wg ON th.worker_group_id = wg.id
LEFT JOIN checks c ON th.check_id = c.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,
worker_group_id,
check_id,
status,
note
) VALUES (
:target_id,
:worker_group_id,
:check_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,77 @@
package workflows
import (
"sort"
"time"
"github.com/mentos1386/zdravko/internal/temporal"
"go.temporal.io/sdk/workflow"
)
func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal.WorkflowCheckParam) (*temporal.WorkflowCheckResult, error) {
workerGroupIds := param.WorkerGroupIds
sort.Strings(workerGroupIds)
targetsFilterResult := temporal.ActivityTargetsFilterResult{}
err := workflow.ExecuteActivity(
workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second,
TaskQueue: temporal.TEMPORAL_SERVER_QUEUE,
}),
temporal.ActivityTargetsFilterName,
temporal.ActivityTargetsFilterParam{
Filter: param.Filter,
},
).Get(ctx, &targetsFilterResult)
if err != nil {
return nil, err
}
for _, target := range targetsFilterResult.Targets {
for _, workerGroupId := range workerGroupIds {
var checkResult *temporal.ActivityCheckResult
err := workflow.ExecuteActivity(
workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second,
TaskQueue: workerGroupId,
}),
temporal.ActivityCheckName,
temporal.ActivityCheckParam{
Script: param.Script,
Target: target,
},
).Get(ctx, &checkResult)
if err != nil {
return nil, err
}
status := temporal.AddTargetHistoryStatusFailure
if checkResult.Success {
status = temporal.AddTargetHistoryStatusSuccess
}
var addTargetHistoryResult *temporal.ActivityAddTargetHistoryResult
err = workflow.ExecuteActivity(
workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second,
TaskQueue: temporal.TEMPORAL_SERVER_QUEUE,
}),
temporal.ActivityAddTargetHistoryName,
&temporal.ActivityAddTargetHistoryParam{
Target: target,
WorkerGroupId: workerGroupId,
CheckId: param.CheckId,
Status: status,
Note: checkResult.Note,
},
).Get(ctx, &addTargetHistoryResult)
if err != nil {
return nil, err
}
}
}
return &temporal.WorkflowCheckResult{
Note: "Check workflow completed",
}, 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,22 @@
package temporal
type AddTargetHistoryStatus string
const (
AddTargetHistoryStatusSuccess AddTargetHistoryStatus = "SUCCESS"
AddTargetHistoryStatusFailure AddTargetHistoryStatus = "FAILURE"
AddTargetHistoryStatusUnknown AddTargetHistoryStatus = "UNKNOWN"
)
type ActivityAddTargetHistoryParam struct {
Target *Target
WorkerGroupId string
CheckId string
Status AddTargetHistoryStatus
Note string
}
type ActivityAddTargetHistoryResult struct {
}
const ActivityAddTargetHistoryName = "ADD_TARGET_HISTORY"

View file

@ -0,0 +1,13 @@
package temporal
type ActivityCheckParam struct {
Script string
Target *Target
}
type ActivityCheckResult struct {
Success bool
Note string
}
const ActivityCheckName = "CHECK"

View file

@ -0,0 +1,11 @@
package temporal
type ActivityTargetsFilterParam struct {
Filter string
}
type ActivityTargetsFilterResult struct {
Targets []*Target
}
const ActivityTargetsFilterName = "TARGETS_FILTER"

View file

@ -5,13 +5,24 @@ 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"
)
type Target struct {
Id string
Name string
Group string
Metadata string
}
// 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,14 @@
package temporal
type WorkflowCheckParam struct {
Script string
Filter string
CheckId string
WorkerGroupIds []string
}
type WorkflowCheckResult struct {
Note 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,37 @@
package activities
import (
"context"
"log/slog"
"github.com/mentos1386/zdravko/internal/temporal"
"github.com/mentos1386/zdravko/pkg/k6"
"github.com/mentos1386/zdravko/pkg/k6/zdravko"
"github.com/mentos1386/zdravko/pkg/script"
"gopkg.in/yaml.v3"
)
func (a *Activities) Check(ctx context.Context, param temporal.ActivityCheckParam) (*temporal.ActivityCheckResult, error) {
execution := k6.NewExecution(slog.Default(), script.UnescapeString(param.Script))
var metadata map[string]interface{}
err := yaml.Unmarshal([]byte(param.Target.Metadata), &metadata)
if err != nil {
return nil, err
}
ctx = zdravko.WithZdravkoContext(ctx, zdravko.Context{
Target: zdravko.Target{
Name: param.Target.Name,
Group: param.Target.Group,
Metadata: metadata,
},
})
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 @@
package api
import "code.tjo.space/mentos1386/zdravko/database/models"
type ApiV1ChecksHistoryPOSTBody struct {
Status models.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)

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

@ -0,0 +1,17 @@
package zdravko
import "context"
type zdravkoContextKey string
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)
}

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

@ -0,0 +1,66 @@
package zdravko
import (
"github.com/dop251/goja"
"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 Target struct {
Name string
Group string
Metadata map[string]interface{}
}
type Zdravko struct {
vu modules.VU
Targets []Target
}
func (z *Zdravko) GetTarget() goja.Value {
zdravkoContext := GetZdravkoContext(z.vu.Context())
return z.vu.Runtime().ToValue(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.AddTargetHistory, activity.RegisterOptions{Name: temporal.ActivityAddTargetHistoryName})
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,
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;
}
@ -987,6 +991,11 @@ video {
background-color: rgb(253 186 116 / var(--tw-bg-opacity));
}
.bg-purple-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 232 255 / var(--tw-bg-opacity));
}
.bg-red-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
@ -1221,6 +1230,11 @@ video {
color: rgb(22 101 52 / var(--tw-text-opacity));
}
.text-purple-800 {
--tw-text-opacity: 1;
color: rgb(107 33 168 / var(--tw-text-opacity));
}
.text-red-600 {
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity));
@ -1278,6 +1292,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 +1426,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 +1441,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 +1456,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 +1464,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));
}
@ -1790,6 +1808,10 @@ code {
}
@media (min-width: 640px) {
.sm\:col-span-2 {
grid-column: span 2 / span 2;
}
.sm\:w-auto {
width: auto;
}

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,47 +57,16 @@
</tr>
</thead>
<tbody>
{{ range .CheckGroups }}
{{ $currentGroup := . }}
<tr class="row-special">
<th scope="rowgroup">
{{ . }}
</th>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{{ range $group, $checks := $.Checks }}
{{ if eq $group $currentGroup }}
{{ range $checks }}
{{ range $checks := .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"
>3</span
>
Unknown
</span>
{{ end }}
</td>
<td>
{{ range .WorkerGroups }}
@ -134,15 +102,10 @@
{{ .Schedule }}
</td>
<td>
<a href="/settings/checks/{{ .Id }}" class="link"
>Details</a
>
<a href="/settings/checks/{{ .Id }}" class="link">Details</a>
</td>
</tr>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
</tbody>
</table>
</section>

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,15 +34,27 @@
<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="sm:col-span-2 h-12">
{{ ScriptUnescapeString .ExampleFilter }}</textarea
>
<div
id="editor"
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
id="editor-filter"
class="hidden sm:col-span-2 block w-full h-12 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p>
<p class="sm:col-span-2">
With filter we specify what targets the check will run on. The must be a
javascript expression that returns a boolean.
</p>
<label for="script">Script</label>
<textarea required id="script" name="script" class="sm:col-span-2 h-96">
{{ ScriptUnescapeString .ExampleScript }}</textarea
>
<div
id="editor-script"
class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p class="sm:col-span-2">
Script is what determines the status of a service. You can read more
about it on
<a target="_blank" href="https://k6.io/docs/using-k6/http-requests/"
@ -75,36 +67,50 @@
<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: "javascript",
options: {
quickSuggestions: false,
},
},
{ 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;
}
}
window.editors = {};
for (const { name, language, options = {} } 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.editor = monaco.editor.create(document.getElementById("editor"), {
value: script,
language: "javascript",
window.editors[name] = monaco.editor.create(editor, {
value: textarea.value,
language: language,
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
wordWrap: "on",
...options,
});
const divElem = document.getElementById("editor");
const resizeObserver = new ResizeObserver((entries) => {
window.editor.layout();
window.editors[name].layout();
});
resizeObserver.observe(divElem);
resizeObserver.observe(editor);
});
}
</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,15 +28,27 @@
<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="sm:col-span-2 h-12">
{{ ScriptUnescapeString .Check.Filter }}</textarea
>
<div
id="editor-filter"
class="hidden sm:col-span-2 block w-full h-12 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p class="sm:col-span-2">
With filter we specify what targets the check will run on. The must be a
javascript expression that returns a boolean.
</p>
<label for="script">Script</label>
<textarea required id="script" name="script" class="h-96">
<textarea required id="script" name="script" class="sm:col-span-2 h-96">
{{ ScriptUnescapeString .Check.Script }}</textarea
>
<div
id="editor"
class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden hidden"
id="editor-script"
class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p>
<p class="sm:col-span-2">
Script is what determines the status of a service. You can read more
about it on
<a target="_blank" href="https://k6.io/docs/using-k6/http-requests/"
@ -135,23 +115,51 @@
</caption>
<thead>
<tr>
<th>Check ID</th>
<th>Status</th>
<th>Worker Group</th>
<th>Created At</th>
<th>Started At</th>
<th>Ended At</th>
<th>Duration</th>
<th>Note</th>
</tr>
</thead>
<tbody>
{{ range .History }}
{{ 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
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .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>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
<td></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 "Completed" }}
bg-purple-100 text-purple-800
{{ else }}
bg-red-100 text-red-800
{{ end }}"
{{ end }}
"
>
{{ .Status }}
</span>
@ -163,51 +171,63 @@
{{ .WorkerGroupName }}
</span>
</td>
<td>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
</td>
<td>{ .Duration }</td>
<td class="whitespace-normal">
{{ .Note }}
</td>
<td>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
<td>{{ .EndTime.Format "2006-01-02 15:04:05" }}</td>
<td>{{ DurationRoundMillisecond .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: "javascript",
options: {
quickSuggestions: false,
},
},
{ 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;
}
}
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, options = {} } 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',
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,
wordWrap: "on",
...options,
});
const divElem = document.getElementById('editor');
const resizeObserver = new ResizeObserver(entries => {
window.editor.layout();
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(divElem);
resizeObserver.observe(editor);
});
</script>
}
</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
@ -44,31 +42,35 @@
</div>
</div>
<section class="mt-4">
<section>
<table>
<caption>
Execution History
<p>Last 10 executions for all checks and worker groups.</p>
History
<p>Last 10 executions.</p>
</caption>
<thead>
<tr>
<th>Check</th>
<th>Worker Group</th>
<th>Check ID</th>
<th>Status</th>
<th>Executed At</th>
<th>Worker Group</th>
<th>Started At</th>
<th>Ended At</th>
<th>Duration</th>
<th>Note</th>
</tr>
</thead>
<tbody>
{{ range .History }}
{{ if eq .Status "Running" }}
<tr>
<th>
<a
class="underline hover:text-blue-600"
href="/settings/checks/{{ .CheckId }}"
>{{ .CheckName }}</a
<td>{{ .CheckId }}</td>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
</th>
{{ .Status }}...
</span>
</td>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
@ -76,25 +78,41 @@
{{ .WorkerGroupName }}
</span>
</td>
<td>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
<td></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
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
{{ if eq .Status "Completed" }}
bg-purple-100 text-purple-800
{{ else }}
bg-red-100 text-red-800
{{ end }}"
{{ end }}
"
>
{{ .Status }}
</span>
</td>
<td>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
</td>
<td class="whitespace-normal">
{{ .Note }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .WorkerGroupName }}
</span>
</td>
<td>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
<td>{{ .EndTime.Format "2006-01-02 15:04:05" }}</td>
<td>{{ DurationRoundMillisecond .Duration }}</td>
<td class="whitespace-normal">{{ .Note }}</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
</section>

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>
{{ range .TargetGroups }}
{{ $currentGroup := . }}
<tr class="row-special">
<th scope="rowgroup">
{{ . }}
</th>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{{ 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"
>
{{ .Type }}
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>
<a href="/settings/targets/{{ .Id }}" class="link">Details</a>
{{ 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>
</tbody>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
</tbody>
</table>
</section>
{{ end }}

View file

@ -0,0 +1,92 @@
{{ 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="sm:col-span-2 h-96"
>
{{ ScriptUnescapeString .Example }}</textarea
>
<div
id="editor-metadata"
class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p class="sm:col-span-2">
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, options = {} } 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,
...options,
});
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(editor);
});
}
</script>
{{ end }}

View file

@ -0,0 +1,203 @@
{{ 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="sm:col-span-2 h-96"
>
{{ ScriptUnescapeString .Target.Metadata }}</textarea
>
<div
id="editor-metadata"
class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p class="sm:col-span-2">
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>Check</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>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .CheckName }}
</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

@ -10,14 +10,14 @@
/>
<p>Name of the trigger can be anything.</p>
<label for="script">Script</label>
<textarea required id="script" name="script" class="h-96">
<textarea required id="script" name="script" class="sm:col-span-2 h-96">
{{ ScriptUnescapeString .Example }}</textarea
>
<div
id="editor"
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
id="editor-script"
class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p>
<p class="sm:col-span-2">
The trigger script executes for every matching <code>target</code>'s
execution of <code>trigger</code>. The outcome of that
<code>trigger</code> is passed to the script as a
@ -30,36 +30,40 @@
<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;
}
}
window.editors = {};
for (const { name, language, options = {} } 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.editor = monaco.editor.create(document.getElementById("editor"), {
value: script,
language: "javascript",
window.editors[name] = monaco.editor.create(editor, {
value: textarea.value,
language: language,
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
...options,
});
const divElem = document.getElementById("editor");
const resizeObserver = new ResizeObserver((entries) => {
window.editor.layout();
window.editors[name].layout();
});
resizeObserver.observe(divElem);
resizeObserver.observe(editor);
});
}
</script>
{{ end }}

View file

@ -3,14 +3,14 @@
<form action="/settings/triggers/{{ .Trigger.Id }}" method="post">
<h2>Configuration</h2>
<label for="script">Script</label>
<textarea required id="script" name="script" class="h-96">
<textarea required id="script" name="script" class="sm:col-span-2 h-96">
{{ ScriptUnescapeString .Trigger.Script }}</textarea
>
<div
id="editor"
class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden hidden"
id="editor-script"
class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div>
<p>
<p class="sm:col-span-2">
The trigger script executes for every matching <code>target</code>'s
execution of <code>trigger</code>. The outcome of that
<code>trigger</code> is passed to the script as a
@ -111,37 +111,41 @@
</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;
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, options = {} } 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',
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,
...options,
});
const divElem = document.getElementById('editor');
const resizeObserver = new ResizeObserver(entries => {
window.editor.layout();
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(divElem);
resizeObserver.observe(editor);
});
</script>
}
</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 *
@ -26,6 +26,12 @@ func load(files ...string) *template.Template {
t := template.New("default").Funcs(
template.FuncMap{
"DurationRoundSecond": func(d time.Duration) time.Duration {
return d.Round(time.Second)
},
"DurationRoundMillisecond": func(d time.Duration) time.Duration {
return d.Round(time.Millisecond)
},
"StringsJoin": strings.Join,
"Now": time.Now,
"ScriptUnescapeString": script.UnescapeString,
@ -51,6 +57,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"),