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

View file

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

View file

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

View file

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

View file

@ -45,15 +45,6 @@ type OAuth2State struct {
ExpiresAt *Time `db:"expires_at"` ExpiresAt *Time `db:"expires_at"`
} }
type CheckStatus string
const (
CheckStatusSuccess CheckStatus = "SUCCESS"
CheckStatusFailure CheckStatus = "FAILURE"
CheckStatusError CheckStatus = "ERROR"
CheckStatusUnknown CheckStatus = "UNKNOWN"
)
type CheckState string type CheckState string
const ( const (
@ -62,45 +53,24 @@ const (
CheckStateUnknown CheckState = "UNKNOWN" CheckStateUnknown CheckState = "UNKNOWN"
) )
type CheckVisibility string
const (
CheckVisibilityPublic CheckVisibility = "PUBLIC"
CheckVisibilityPrivate CheckVisibility = "PRIVATE"
CheckVisibilityUnknown CheckVisibility = "UNKNOWN"
)
type Check struct { type Check struct {
CreatedAt *Time `db:"created_at"` CreatedAt *Time `db:"created_at"`
UpdatedAt *Time `db:"updated_at"` UpdatedAt *Time `db:"updated_at"`
Id string `db:"id"` Id string `db:"id"`
Name string `db:"name"` Name string `db:"name"`
Group string `db:"group"`
Visibility CheckVisibility `db:"visibility"`
Schedule string `db:"schedule"` Schedule string `db:"schedule"`
Script string `db:"script"` Script string `db:"script"`
Filter string `db:"filter"`
} }
type CheckWithWorkerGroups struct { type CheckWithWorkerGroups struct {
Check Check
// List of worker group names // List of worker group names
WorkerGroups []string 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 { type WorkerGroup struct {
CreatedAt *Time `db:"created_at"` CreatedAt *Time `db:"created_at"`
UpdatedAt *Time `db:"updated_at"` UpdatedAt *Time `db:"updated_at"`
@ -116,15 +86,6 @@ type WorkerGroupWithChecks struct {
Checks []string Checks []string
} }
type TriggerStatus string
const (
TriggerStatusSuccess TriggerStatus = "SUCCESS"
TriggerStatusFailure TriggerStatus = "FAILURE"
TriggerStatusError TriggerStatus = "ERROR"
TriggerStatusUnknown TriggerStatus = "UNKNOWN"
)
type TriggerState string type TriggerState string
const ( const (
@ -133,14 +94,6 @@ const (
TriggerStateUnknown TriggerState = "UNKNOWN" TriggerStateUnknown TriggerState = "UNKNOWN"
) )
type TriggerVisibility string
const (
TriggerVisibilityPublic TriggerVisibility = "PUBLIC"
TriggerVisibilityPrivate TriggerVisibility = "PRIVATE"
TriggerVisibilityUnknown TriggerVisibility = "UNKNOWN"
)
type Trigger struct { type Trigger struct {
CreatedAt *Time `db:"created_at"` CreatedAt *Time `db:"created_at"`
UpdatedAt *Time `db:"updated_at"` UpdatedAt *Time `db:"updated_at"`
@ -150,10 +103,49 @@ type Trigger struct {
Script string `db:"script"` 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"` CreatedAt *Time `db:"created_at"`
TriggerId string `db:"trigger_id"` TargetId string `db:"target_id"`
Status TriggerStatus `db:"status"` WorkerGroupId string `db:"worker_group_id"`
Note string `db:"note"` 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 ( CREATE TABLE checks (
id TEXT NOT NULL, id TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
"group" TEXT NOT NULL,
schedule TEXT NOT NULL, schedule TEXT NOT NULL,
script 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')), 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')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
@ -21,8 +20,6 @@ CREATE TABLE checks (
PRIMARY KEY (id), PRIMARY KEY (id),
CONSTRAINT unique_checks_name UNIQUE (name) CONSTRAINT unique_checks_name UNIQUE (name)
) STRICT; ) STRICT;
-- +migrate StatementBegin -- +migrate StatementBegin
CREATE TRIGGER checks_updated_timestamp AFTER UPDATE ON checks BEGIN 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; 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), PRIMARY KEY (id),
CONSTRAINT unique_worker_groups_name UNIQUE (name) CONSTRAINT unique_worker_groups_name UNIQUE (name)
) STRICT; ) STRICT;
-- +migrate StatementBegin -- +migrate StatementBegin
CREATE TRIGGER worker_groups_updated_timestamp AFTER UPDATE ON worker_groups BEGIN 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; 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 CONSTRAINT fk_check_worker_groups_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE
) STRICT; ) 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 ( CREATE TABLE triggers (
id TEXT NOT NULL, id TEXT NOT NULL,
@ -80,24 +63,48 @@ CREATE TABLE triggers (
PRIMARY KEY (id), PRIMARY KEY (id),
CONSTRAINT unique_triggers_name UNIQUE (name) CONSTRAINT unique_triggers_name UNIQUE (name)
) STRICT; ) STRICT;
-- +migrate StatementBegin -- +migrate StatementBegin
CREATE TRIGGER triggers_updated_timestamp AFTER UPDATE ON triggers BEGIN 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; UPDATE triggers SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = NEW.id;
END; END;
-- +migrate StatementEnd -- +migrate StatementEnd
CREATE TABLE trigger_histories ( CREATE TABLE targets (
trigger_id TEXT NOT NULL, 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, status TEXT NOT NULL,
note TEXT NOT NULL, note TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (trigger_id, created_at), PRIMARY KEY (target_id, worker_group_id, check_id, created_at),
CONSTRAINT fk_trigger_histories_trigger FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE 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; ) STRICT;
-- +migrate Down -- +migrate Down
@ -105,9 +112,7 @@ DROP TABLE oauth2_states;
DROP TABLE check_worker_groups; DROP TABLE check_worker_groups;
DROP TABLE worker_groups; DROP TABLE worker_groups;
DROP TRIGGER worker_groups_updated_timestamp; DROP TRIGGER worker_groups_updated_timestamp;
DROP TABLE check_histories;
DROP TABLE checks; DROP TABLE checks;
DROP TRIGGER checks_updated_timestamp; DROP TRIGGER checks_updated_timestamp;
DROP TABLE triggers; DROP TABLE triggers;
DROP TABLE trigger_histories;
DROP TRIGGER triggers_updated_timestamp; DROP TRIGGER triggers_updated_timestamp;

View file

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

View file

@ -7,8 +7,11 @@
SESSION_SECRET=your_secret SESSION_SECRET=your_secret
# To generate keys, run "just generate-jwt-key" # To generate keys, run "just generate-jwt-key"
JWT_PUBLIC_KEY="" # When running `just run` or `just run-worker`
JWT_PRIVATE_KEY="" # 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 # To generate worker token, go to website and
# create new worker. Then copy the token. # 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 go 1.21.6
require ( require (
github.com/dgraph-io/badger/v4 v4.2.0 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/go-playground/validator/v10 v10.18.0
github.com/golang-jwt/jwt/v5 v5.2.0 github.com/golang-jwt/jwt/v5 v5.2.0
github.com/gorilla/sessions v1.2.2 github.com/gorilla/sessions v1.2.2
@ -20,7 +21,7 @@ require (
go.temporal.io/api v1.27.0 go.temporal.io/api v1.27.0
go.temporal.io/sdk v1.26.0-rc.2 go.temporal.io/sdk v1.26.0-rc.2
go.temporal.io/server v1.22.4 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 golang.org/x/oauth2 v0.17.0
gopkg.in/yaml.v2 v2.4.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-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.9.0 // 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/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // 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/fx v1.20.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.19.0 // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/mod v0.15.0 // indirect golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.21.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.6.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.17.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.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 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/api v0.155.0 // indirect google.golang.org/api v0.155.0 // indirect
google.golang.org/appengine v1.6.8 // 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-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-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.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.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-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-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-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-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-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-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-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 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-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-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 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.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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/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.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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/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.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.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.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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= 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.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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 ( import (
"net/http" "net/http"
"code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4" "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" "strings"
"time" "time"
jwtInternal "code.tjo.space/mentos1386/zdravko/internal/jwt"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
jwtInternal "github.com/mentos1386/zdravko/pkg/jwt"
) )
const sessionName = "zdravko-hey" const authenticationSessionName = "zdravko-hey"
type AuthenticatedPrincipal struct { type AuthenticatedPrincipal struct {
User *AuthenticatedUser User *AuthenticatedUser
@ -48,7 +48,7 @@ func GetUser(ctx context.Context) *AuthenticatedUser {
} }
func (h *BaseHandler) AuthenticateRequestWithCookies(r *http.Request) (*AuthenticatedUser, error) { 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 { if err != nil {
return nil, err 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 { 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 { if err != nil {
return err 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_refresh_token"] = user.OAuth2RefreshToken
session.Values["oauth2_token_type"] = user.OAuth2TokenType session.Values["oauth2_token_type"] = user.OAuth2TokenType
session.Values["oauth2_expiry"] = user.OAuth2Expiry.Format(time.RFC3339) session.Values["oauth2_expiry"] = user.OAuth2Expiry.Format(time.RFC3339)
err = h.store.Save(r, w, session) return h.store.Save(r, w, session)
if err != nil {
return err
}
return nil
} }
func (h *BaseHandler) ClearAuthenticatedUserForRequest(w http.ResponseWriter, r *http.Request) error { 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 { if err != nil {
return err return err
} }
session.Options.MaxAge = -1 session.Options.MaxAge = -1
err = h.store.Save(r, w, session) return h.store.Save(r, w, session)
if err != nil {
return err
}
return nil
} }
type AuthenticatedHandler func(http.ResponseWriter, *http.Request, *AuthenticatedPrincipal) 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()) { if user.OAuth2Expiry.Before(time.Now()) {
user, err = h.RefreshToken(c.Response(), c.Request(), user) user, err = h.RefreshToken(c.Response(), c.Request(), user)
if err != nil { 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 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 # Example trigger code
trigger: | trigger: |
import kv from 'zdravko/kv'; import kv from 'k6/x/zdravko/kv';
import incidents, { severity } from 'zdravko/incidents'; import incidents, { severity } from 'k6/x/zdravko/incidents';
import { getTarget, getMonitor, getOutcome } from 'k6/x/zdravko';
// Only execute on this specific targets.
export function filter(target) {
return target.tags.kind === 'http';
}
const getMinute = (date) => { const getMinute = (date) => {
return Math.floor(date.getTime() / 1000 / 60); 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 // This trigger will check if there were more than 5 issues in last
// 5 minutes, if so it will create a critical incident. // 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 the outcome is not failure, we close any potential incidents.
if (outcome.status !== 'FAILURE') { if (outcome.status !== 'FAILURE') {
incidents.close(target, monitor); incidents.close(target, monitor);
@ -62,6 +62,7 @@ trigger: |
# Example monitor code # Example monitor code
check: | check: |
import http from 'k6/http'; import http from 'k6/http';
import { getTarget } from 'k6/x/zdravko';
export const options = { export const options = {
thresholds: { 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. // Execute the check on the targets.
export default function (target) { export default function () {
http.get(target.url); 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" "embed"
"log/slog" "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/gorilla/sessions"
"github.com/jmoiron/sqlx" "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" "go.temporal.io/sdk/client"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -18,8 +18,10 @@ import (
var examplesYaml embed.FS var examplesYaml embed.FS
type examples struct { type examples struct {
Check string `yaml:"check"` Check string `yaml:"check"`
Filter string `yaml:"filter"`
Trigger string `yaml:"trigger"` Trigger string `yaml:"trigger"`
Target string `yaml:"target"`
} }
var Pages = []*components.Page{ var Pages = []*components.Page{
@ -39,7 +41,7 @@ func GetPageByTitle(pages []*components.Page, title string) *components.Page {
type BaseHandler struct { type BaseHandler struct {
db *sqlx.DB db *sqlx.DB
kvStore kv.KeyValueStore kvStore database.KeyValueStore
config *config.ServerConfig config *config.ServerConfig
logger *slog.Logger logger *slog.Logger
@ -50,7 +52,7 @@ type BaseHandler struct {
examples examples 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)) store := sessions.NewCookieStore([]byte(config.SessionSecret))
examples := examples{} examples := examples{}
@ -64,7 +66,9 @@ func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Clien
} }
examples.Check = script.EscapeString(examples.Check) examples.Check = script.EscapeString(examples.Check)
examples.Filter = script.EscapeString(examples.Filter)
examples.Trigger = script.EscapeString(examples.Trigger) examples.Trigger = script.EscapeString(examples.Trigger)
examples.Target = script.EscapeString(examples.Target)
return &BaseHandler{ return &BaseHandler{
db: db, db: db,

View file

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

View file

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

View file

@ -12,13 +12,52 @@ import (
"strconv" "strconv"
"time" "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/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" "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 { type UserInfo struct {
Id int `json:"id"` // FIXME: This might not always be int? Id int `json:"id"` // FIXME: This might not always be int?
Sub string `json:"sub"` Sub string `json:"sub"`
@ -97,6 +136,14 @@ func (h *BaseHandler) OAuth2LoginGET(c echo.Context) error {
url := conf.AuthCodeURL(state, oauth2.AccessTypeOffline) 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) return c.Redirect(http.StatusTemporaryRedirect, url)
} }
@ -156,7 +203,21 @@ func (h *BaseHandler) OAuth2CallbackGET(c echo.Context) error {
return err 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 { func (h *BaseHandler) OAuth2LogoutGET(c echo.Context) error {

View file

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

View file

@ -5,33 +5,31 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"net/http" "net/http"
"slices"
"strings" "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/go-playground/validator/v10"
"github.com/gosimple/slug" "github.com/gosimple/slug"
"github.com/labstack/echo/v4" "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 { type CreateCheck struct {
Name string `validate:"required"` Name string `validate:"required"`
Group string `validate:"required"`
WorkerGroups string `validate:"required"` WorkerGroups string `validate:"required"`
Schedule string `validate:"required,cron"` Schedule string `validate:"required,cron"`
Script string `validate:"required"` Script string `validate:"required"`
Visibility string `validate:"required,oneof=PUBLIC PRIVATE"` Filter string `validate:"required"`
} }
type UpdateCheck struct { type UpdateCheck struct {
Group string `validate:"required"`
WorkerGroups string `validate:"required"` WorkerGroups string `validate:"required"`
Schedule string `validate:"required,cron"` Schedule string `validate:"required,cron"`
Script string `validate:"required"` Script string `validate:"required"`
Visibility string `validate:"required,oneof=PUBLIC PRIVATE"` Filter string `validate:"required"`
} }
type CheckWithWorkerGroupsAndState struct { type CheckWithWorkerGroupsAndState struct {
@ -41,19 +39,24 @@ type CheckWithWorkerGroupsAndState struct {
type SettingsChecks struct { type SettingsChecks struct {
*Settings *Settings
Checks map[string][]*CheckWithWorkerGroupsAndState Checks []*CheckWithWorkerGroupsAndState
CheckGroups []string History []struct {
CreatedAt time.Time
Status string
Note string
}
} }
type SettingsCheck struct { type SettingsCheck struct {
*Settings *Settings
Check *CheckWithWorkerGroupsAndState Check *CheckWithWorkerGroupsAndState
History []*models.CheckHistory History []*services.CheckHistory
} }
type SettingsCheckCreate struct { type SettingsCheckCreate struct {
*Settings *Settings
Example string ExampleScript string
ExampleFilter string
} }
func (h *BaseHandler) SettingsChecksGET(c echo.Context) error { 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 { for i, check := range checks {
state, err := services.GetCheckState(context.Background(), h.temporal, check.Id) state, err := services.GetCheckState(context.Background(), h.temporal, check.Id)
if err != nil { if err != nil {
return err h.logger.Error("Failed to get check state", "error", err)
state = models.CheckStateUnknown
} }
checksWithState[i] = &CheckWithWorkerGroupsAndState{ checksWithState[i] = &CheckWithWorkerGroupsAndState{
CheckWithWorkerGroups: check, 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{ return c.Render(http.StatusOK, "settings_checks.tmpl", &SettingsChecks{
Settings: NewSettings( Settings: NewSettings(
cc.Principal.User, cc.Principal.User,
GetPageByTitle(SettingsPages, "Checks"), GetPageByTitle(SettingsPages, "Checks"),
[]*components.Page{GetPageByTitle(SettingsPages, "Checks")}, []*components.Page{GetPageByTitle(SettingsPages, "Checks")},
), ),
Checks: checksByGroup, Checks: checksWithState,
CheckGroups: checkGroups,
}) })
} }
@ -116,7 +110,7 @@ func (h *BaseHandler) SettingsChecksDescribeGET(c echo.Context) error {
State: status, State: status,
} }
history, err := services.GetCheckHistoryForCheck(context.Background(), h.db, slug) history, err := services.GetCheckHistoryForCheck(context.Background(), h.temporal, slug)
if err != nil { if err != nil {
return err return err
} }
@ -196,11 +190,10 @@ func (h *BaseHandler) SettingsChecksDescribePOST(c echo.Context) error {
checkId := c.Param("id") checkId := c.Param("id")
update := UpdateCheck{ update := UpdateCheck{
Group: strings.ToLower(c.FormValue("group")),
WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))), WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))),
Schedule: c.FormValue("schedule"), Schedule: c.FormValue("schedule"),
Script: script.EscapeString(c.FormValue("script")), Script: script.EscapeString(c.FormValue("script")),
Visibility: c.FormValue("visibility"), Filter: c.FormValue("filter"),
} }
err := validator.New(validator.WithRequiredStructEnabled()).Struct(update) err := validator.New(validator.WithRequiredStructEnabled()).Struct(update)
if err != nil { if err != nil {
@ -211,10 +204,9 @@ func (h *BaseHandler) SettingsChecksDescribePOST(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
check.Group = update.Group
check.Schedule = update.Schedule check.Schedule = update.Schedule
check.Script = update.Script check.Script = update.Script
check.Visibility = models.CheckVisibility(update.Visibility) check.Filter = update.Filter
err = services.UpdateCheck( err = services.UpdateCheck(
ctx, ctx,
@ -270,7 +262,8 @@ func (h *BaseHandler) SettingsChecksCreateGET(c echo.Context) error {
GetPageByTitle(SettingsPages, "Checks Create"), 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{ create := CreateCheck{
Name: c.FormValue("name"), Name: c.FormValue("name"),
Group: strings.ToLower(c.FormValue("group")),
WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))), WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))),
Schedule: c.FormValue("schedule"), Schedule: c.FormValue("schedule"),
Script: script.EscapeString(c.FormValue("script")), Script: script.EscapeString(c.FormValue("script")),
Visibility: c.FormValue("visibility"), Filter: c.FormValue("filter"),
} }
err := validator.New(validator.WithRequiredStructEnabled()).Struct(create) err := validator.New(validator.WithRequiredStructEnabled()).Struct(create)
if err != nil { if err != nil {
@ -312,12 +304,11 @@ func (h *BaseHandler) SettingsChecksCreatePOST(c echo.Context) error {
} }
check := &models.Check{ check := &models.Check{
Name: create.Name, Name: create.Name,
Group: create.Group, Id: checkId,
Id: checkId, Schedule: create.Schedule,
Schedule: create.Schedule, Script: create.Script,
Script: create.Script, Filter: create.Filter,
Visibility: models.CheckVisibility(create.Visibility),
} }
err = services.CreateCheck( err = services.CreateCheck(

View file

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

View file

@ -3,7 +3,7 @@ package handlers
import ( import (
"net/http" "net/http"
"code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4" "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" "fmt"
"net/http" "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/go-playground/validator/v10"
"github.com/gosimple/slug" "github.com/gosimple/slug"
"github.com/labstack/echo/v4" "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 { type CreateTrigger struct {
@ -36,7 +36,7 @@ type SettingsTriggers struct {
type SettingsTrigger struct { type SettingsTrigger struct {
*Settings *Settings
Trigger *TriggerWithState Trigger *TriggerWithState
History []*models.TriggerHistory History []*services.TriggerHistory
} }
type SettingsTriggerCreate struct { type SettingsTriggerCreate struct {

View file

@ -6,13 +6,13 @@ import (
"net/http" "net/http"
"strings" "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/go-playground/validator/v10"
"github.com/gosimple/slug" "github.com/gosimple/slug"
"github.com/labstack/echo/v4" "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 { type WorkerWithTokenAndActiveWorkers struct {
@ -52,7 +52,7 @@ func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error {
} }
workerGroupsWithActiveWorkers[i] = &WorkerGroupWithActiveWorkers{ workerGroupsWithActiveWorkers[i] = &WorkerGroupWithActiveWorkers{
WorkerGroupWithChecks: workerGroup, WorkerGroupWithChecks: workerGroup,
ActiveWorkers: activeWorkers, ActiveWorkers: activeWorkers,
} }
} }

View file

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

View file

@ -6,9 +6,9 @@ import (
"sort" "sort"
"time" "time"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/internal/workflows"
"github.com/jmoiron/sqlx" "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/client"
"go.temporal.io/sdk/temporal" "go.temporal.io/sdk/temporal"
"golang.org/x/exp/maps" "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 { func CreateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error {
_, err := db.NamedExecContext(ctx, _, err := db.NamedExecContext(ctx,
`INSERT INTO checks (id, name, visibility, "group", script, schedule) `INSERT INTO checks (id, name, script, schedule, filter)
VALUES (:id, :name, :visibility, :group, :script, :schedule)`, VALUES (:id, :name, :script, :schedule, :filter)`,
check, check,
) )
return err 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 { func UpdateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error {
_, err := db.NamedExecContext(ctx, _, 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, check,
) )
return err return err
@ -120,12 +120,11 @@ func GetCheckWithWorkerGroups(ctx context.Context, db *sqlx.DB, id string) (*mod
SELECT SELECT
checks.id, checks.id,
checks.name, checks.name,
checks.visibility,
checks."group",
checks.script, checks.script,
checks.schedule, checks.schedule,
checks.created_at, checks.created_at,
checks.updated_at, checks.updated_at,
checks.filter,
worker_groups.name as worker_group_name worker_groups.name as worker_group_name
FROM checks FROM checks
LEFT OUTER JOIN check_worker_groups ON checks.id = check_worker_groups.check_id 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( err = rows.Scan(
&check.Id, &check.Id,
&check.Name, &check.Name,
&check.Visibility,
&check.Group,
&check.Script, &check.Script,
&check.Schedule, &check.Schedule,
&check.CreatedAt, &check.CreatedAt,
&check.UpdatedAt, &check.UpdatedAt,
&check.Filter,
&workerGroupName, &workerGroupName,
) )
if err != nil { if err != nil {
@ -180,12 +178,11 @@ func GetChecksWithWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.Chec
SELECT SELECT
checks.id, checks.id,
checks.name, checks.name,
checks.visibility,
checks."group",
checks.script, checks.script,
checks.schedule, checks.schedule,
checks.created_at, checks.created_at,
checks.updated_at, checks.updated_at,
checks.filter,
worker_groups.name as worker_group_name worker_groups.name as worker_group_name
FROM checks FROM checks
LEFT OUTER JOIN check_worker_groups ON checks.id = check_worker_groups.check_id 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( err = rows.Scan(
&check.Id, &check.Id,
&check.Name, &check.Name,
&check.Visibility,
&check.Group,
&check.Script, &check.Script,
&check.Schedule, &check.Schedule,
&check.CreatedAt, &check.CreatedAt,
&check.UpdatedAt, &check.UpdatedAt,
&check.Filter,
&workerGroupName, &workerGroupName,
) )
if err != nil { if err != nil {
@ -254,7 +250,8 @@ func CreateOrUpdateCheckSchedule(
} }
args := make([]interface{}, 1) args := make([]interface{}, 1)
args[0] = workflows.CheckWorkflowParam{ args[0] = internaltemporal.WorkflowCheckParam{
Filter: check.Filter,
Script: check.Script, Script: check.Script,
CheckId: check.Id, CheckId: check.Id,
WorkerGroupIds: workerGroupStrings, WorkerGroupIds: workerGroupStrings,
@ -268,9 +265,9 @@ func CreateOrUpdateCheckSchedule(
}, },
Action: &client.ScheduleWorkflowAction{ Action: &client.ScheduleWorkflowAction{
ID: getScheduleId(check.Id), ID: getScheduleId(check.Id),
Workflow: workflows.NewWorkflows(nil).CheckWorkflowDefinition, Workflow: internaltemporal.WorkflowCheckName,
Args: args, Args: args,
TaskQueue: "default", TaskQueue: internaltemporal.TEMPORAL_SERVER_QUEUE,
RetryPolicy: &temporal.RetryPolicy{ RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 3, 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 ( import (
"context" "context"
"code.tjo.space/mentos1386/zdravko/database/models" "github.com/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx" "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 ( import (
"context" "context"
"code.tjo.space/mentos1386/zdravko/database/models" "github.com/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx" "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 ( import (
"context" "context"
"code.tjo.space/mentos1386/zdravko/database/models" "github.com/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"go.temporal.io/api/enums/v1" "go.temporal.io/api/enums/v1"
"go.temporal.io/sdk/client" "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" "log/slog"
"time" "time"
"code.tjo.space/mentos1386/zdravko/internal/config" "github.com/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/jwt" "github.com/mentos1386/zdravko/pkg/jwt"
"code.tjo.space/mentos1386/zdravko/pkg/retry" "github.com/mentos1386/zdravko/pkg/retry"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.temporal.io/sdk/client" "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 { type AuthHeadersProvider struct {
Token string 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" echo "Created migration file: $FILENAME"
# Generate and download all external dependencies. # Generate and download all external dependencies.
generate: generate: _tailwindcss-build _htmx-download _monaco-download _feather-icons-download
go generate ./... go generate ./...
_tailwindcss-build: _tailwindcss-build:
@ -119,11 +119,12 @@ _monaco-download:
mv node_modules/monaco-editor/min {{STATIC_DIR}}/monaco mv node_modules/monaco-editor/min {{STATIC_DIR}}/monaco
rm -rf node_modules rm -rf node_modules
# We onlt care about javascript language # We only care about javascript language
find {{STATIC_DIR}}/monaco/vs/basic-languages/ \ find {{STATIC_DIR}}/monaco/vs/basic-languages/ \
-type d \ -type d \
-not -name 'javascript' \ -not -name 'javascript' \
-not -name 'typescript' \ -not -name 'typescript' \
-not -name 'yaml' \
-not -name 'basic-languages' \ -not -name 'basic-languages' \
-prune -exec rm -rf {} \; -prune -exec rm -rf {} \;

View file

@ -1,9 +1 @@
package api 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" "encoding/hex"
"time" "time"
"code.tjo.space/mentos1386/zdravko/database/models" "github.com/mentos1386/zdravko/database/models"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/pkg/errors" "github.com/pkg/errors"
) )

View file

@ -5,6 +5,8 @@ import (
"log/slog" "log/slog"
"os" "os"
"testing" "testing"
"github.com/mentos1386/zdravko/pkg/k6/zdravko"
) )
func getLogger() *slog.Logger { func getLogger() *slog.Logger {
@ -18,12 +20,12 @@ func getLogger() *slog.Logger {
} }
func TestK6Success(t *testing.T) { func TestK6Success(t *testing.T) {
ctx := context.Background()
logger := getLogger() logger := getLogger()
script := ` script := `
import http from 'k6/http'; import http from 'k6/http';
import { sleep } from 'k6'; import { sleep } from 'k6';
import { getTarget } from 'k6/x/zdravko';
export const options = { export const options = {
vus: 10, vus: 10,
@ -31,6 +33,8 @@ export const options = {
}; };
export default function () { export default function () {
const target = getTarget();
console.log('Target:', target);
http.get('https://test.k6.io'); http.get('https://test.k6.io');
sleep(1); sleep(1);
} }
@ -38,6 +42,14 @@ export default function () {
execution := NewExecution(logger, script) 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) result, err := execution.Run(ctx)
if err != nil { if err != nil {
t.Errorf("Error starting execution: %v", err) 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" "log/slog"
"net/http" "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/jmoiron/sqlx"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "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" "go.temporal.io/sdk/client"
) )
func Routes( func Routes(
e *echo.Echo, e *echo.Echo,
sqlDb *sqlx.DB, sqlDb *sqlx.DB,
kvStore kv.KeyValueStore, kvStore database.KeyValueStore,
temporalClient client.Client, temporalClient client.Client,
cfg *config.ServerConfig, cfg *config.ServerConfig,
logger *slog.Logger, logger *slog.Logger,
@ -64,13 +64,13 @@ func Routes(
settings.GET("/triggers/:id/enable", h.SettingsTriggersEnableGET) settings.GET("/triggers/:id/enable", h.SettingsTriggersEnableGET)
settings.GET("/targets", h.SettingsTargetsGET) settings.GET("/targets", h.SettingsTargetsGET)
//settings.GET("/targets/create", h.SettingsTargetsCreateGET) settings.GET("/targets/create", h.SettingsTargetsCreateGET)
//settings.POST("/targets/create", h.SettingsTargetsCreatePOST) settings.POST("/targets/create", h.SettingsTargetsCreatePOST)
//settings.GET("/targets/:id", h.SettingsTargetsDescribeGET) settings.GET("/targets/:id", h.SettingsTargetsDescribeGET)
//settings.POST("/targets/:id", h.SettingsTargetsDescribePOST) settings.POST("/targets/:id", h.SettingsTargetsDescribePOST)
//settings.GET("/targets/:id/delete", h.SettingsTargetsDescribeDELETE) settings.GET("/targets/:id/delete", h.SettingsTargetsDescribeDELETE)
//settings.GET("/targets/:id/disable", h.SettingsTargetsDisableGET) settings.GET("/targets/:id/disable", h.SettingsTargetsDisableGET)
//settings.GET("/targets/:id/enable", h.SettingsTargetsEnableGET) settings.GET("/targets/:id/enable", h.SettingsTargetsEnableGET)
settings.GET("/incidents", h.SettingsIncidentsGET) settings.GET("/incidents", h.SettingsIncidentsGET)
@ -103,7 +103,6 @@ func Routes(
apiv1 := e.Group("/api/v1") apiv1 := e.Group("/api/v1")
apiv1.Use(h.Authenticated) apiv1.Use(h.Authenticated)
apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET) apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET)
apiv1.POST("/checks/:id/history", h.ApiV1ChecksHistoryPOST)
// Error handler // Error handler
e.HTTPErrorHandler = func(err error, c echo.Context) { e.HTTPErrorHandler = func(err error, c echo.Context) {

View file

@ -4,13 +4,12 @@ import (
"context" "context"
"log/slog" "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"
"github.com/labstack/echo/v4/middleware" "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" "github.com/pkg/errors"
) )
@ -45,12 +44,12 @@ func (s *Server) Start() error {
return errors.Wrap(err, "failed to connect to temporal") 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 { if err != nil {
return errors.Wrap(err, "failed to open kv store") 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.Renderer = templates.NewTemplates()
s.echo.Use(middleware.Logger()) s.echo.Use(middleware.Logger())

View file

@ -1,34 +1,45 @@
package server package server
import ( import (
"code.tjo.space/mentos1386/zdravko/internal/activities" "log/slog"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/workflows" "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/client"
"go.temporal.io/sdk/worker" temporalWorker "go.temporal.io/sdk/worker"
"go.temporal.io/sdk/workflow"
) )
type Worker struct { type Worker struct {
worker worker.Worker worker temporalWorker.Worker
} }
func NewWorker(temporalClient client.Client, cfg *config.ServerConfig) *Worker { func NewWorker(temporalClient client.Client, cfg *config.ServerConfig, logger *slog.Logger, db *sqlx.DB, kvStore database.KeyValueStore) *Worker {
w := worker.New(temporalClient, "default", worker.Options{}) 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 // 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{ return &Worker{
worker: w, worker: worker,
} }
} }
func (w *Worker) Start() error { func (w *Worker) Start() error {
return w.worker.Run(worker.InterruptCh()) return w.worker.Run(temporalWorker.InterruptCh())
} }
func (w *Worker) Stop() { func (w *Worker) Stop() {

View file

@ -6,8 +6,8 @@ import (
"fmt" "fmt"
"time" "time"
internal "code.tjo.space/mentos1386/zdravko/internal/config" internal "github.com/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/jwt" "github.com/mentos1386/zdravko/pkg/jwt"
"go.temporal.io/server/common/cluster" "go.temporal.io/server/common/cluster"
"go.temporal.io/server/common/config" "go.temporal.io/server/common/config"
"go.temporal.io/server/common/persistence/sql/sqlplugin/sqlite" "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) { func NewServer(cfg *config.Config, tokenKeyProvider authorization.TokenKeyProvider) (t.Server, error) {
logger := log.NewZapLogger(log.BuildZapLogger(log.Config{ logger := log.NewZapLogger(log.BuildZapLogger(log.Config{
Stdout: true, Stdout: true,
Level: "info", Level: "warn",
OutputFile: "", OutputFile: "",
})) }))

View file

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

View file

@ -1,7 +1,7 @@
package temporal package temporal
import ( 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"
"github.com/temporalio/ui-server/v2/server/config" "github.com/temporalio/ui-server/v2/server/config"
"github.com/temporalio/ui-server/v2/server/server_options" "github.com/temporalio/ui-server/v2/server/server_options"

View file

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

View file

@ -64,11 +64,11 @@ code {
@apply bg-blue-700 text-white; @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 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; @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; @apply bg-white hover:bg-gray-300 shadow;
} }

View file

@ -722,6 +722,10 @@ video {
display: none; display: none;
} }
.h-12 {
height: 3rem;
}
.h-20 { .h-20 {
height: 5rem; height: 5rem;
} }
@ -987,6 +991,11 @@ video {
background-color: rgb(253 186 116 / var(--tw-bg-opacity)); 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 { .bg-red-100 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity)); background-color: rgb(254 226 226 / var(--tw-bg-opacity));
@ -1221,6 +1230,11 @@ video {
color: rgb(22 101 52 / var(--tw-text-opacity)); 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 { .text-red-600 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity)); 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: 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-all {
transition-property: all; transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 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)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.checks .time-range > a { .targets .time-range > a {
border-radius: 0.5rem; border-radius: 0.5rem;
padding-left: 0.625rem; padding-left: 0.625rem;
padding-right: 0.625rem; padding-right: 0.625rem;
@ -1423,12 +1441,12 @@ code {
color: rgb(0 0 0 / var(--tw-text-opacity)); color: rgb(0 0 0 / var(--tw-text-opacity));
} }
.checks .time-range > a:hover { .targets .time-range > a:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity)); 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: 2px solid transparent;
outline-offset: 2px; outline-offset: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --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)); --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; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 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); --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); 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; --tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity)); background-color: rgb(209 213 219 / var(--tw-bg-opacity));
} }
@ -1790,6 +1808,10 @@ code {
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:col-span-2 {
grid-column: span 2 / span 2;
}
.sm\:w-auto { .sm\:w-auto {
width: 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" }} {{ define "main" }}
<div class="container max-w-screen-md flex flex-col mt-20 gap-20"> <div class="container max-w-screen-md flex flex-col mt-20 gap-20">
{{ $length := len .Checks }} {{ $length := len .Targets }}
{{ if eq $length 0 }} {{ if eq $length 0 }}
<section> <section>
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16"> <div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1 <h1
class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl" 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> </h1>
<p <p
class="mb-8 text-l font-normal text-gray-700 lg:text-l sm:px-8 lg:px-40" 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 Create a target to target your services and get notified when they
down. are down.
</p> </p>
<div class="flex flex-col gap-4 sm:flex-row sm:justify-center"> <div class="flex flex-col gap-4 sm:flex-row sm:justify-center">
<a <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" 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"> <svg class="feather ml-1 h-5 w-5 overflow-visible">
<use href="/static/icons/feather-sprite.svg#plus" /> <use href="/static/icons/feather-sprite.svg#plus" />
</svg> </svg>
@ -69,7 +69,7 @@
</p> </p>
</div> </div>
{{ end }} {{ end }}
<div class="checks flex flex-col gap-4"> <div class="targets flex flex-col gap-4">
<div <div
class="inline-flex gap-1 justify-center md:justify-end time-range" class="inline-flex gap-1 justify-center md:justify-end time-range"
role="group" role="group"
@ -93,7 +93,7 @@
>90 Minutes</a >90 Minutes</a
> >
</div> </div>
{{ range $group, $checksAndStatus := .Checks }} {{ range $group, $targetsAndStatus := .Targets }}
<details <details
open open
class="bg-white shadow-md rounded-lg p-6 py-4 gap-2 [&_svg]:open:rotate-90" class="bg-white shadow-md rounded-lg p-6 py-4 gap-2 [&_svg]:open:rotate-90"
@ -101,11 +101,11 @@
<summary <summary
class="flex flex-row gap-2 p-3 py-2 -mx-3 cursor-pointer hover:bg-blue-50 rounded-lg" 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 <span
class="flex w-3 h-3 bg-green-400 rounded-full self-center" class="flex w-3 h-3 bg-green-400 rounded-full self-center"
></span> ></span>
{{ else if eq $checksAndStatus.Status "FAILURE" }} {{ else if eq $targetsAndStatus.Status "FAILURE" }}
<span <span
class="flex w-3 h-3 bg-red-400 rounded-full self-center" class="flex w-3 h-3 bg-red-400 rounded-full self-center"
></span> ></span>
@ -123,7 +123,7 @@
<use href="/static/icons/feather-sprite.svg#chevron-right" /> <use href="/static/icons/feather-sprite.svg#chevron-right" />
</svg> </svg>
</summary> </summary>
{{ range $checksAndStatus.Checks }} {{ range $targetsAndStatus.Targets }}
<div <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" 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> </caption>
<thead class="text-xs text-gray-700 uppercase bg-gray-50"> <thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr> <tr>
<th scope="col">Check Group</th>
<th scope="col">Name</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">Worker Groups</th>
<th scope="col">State</th> <th scope="col">State</th>
<th scope="col">Schedule</th> <th scope="col">Schedule</th>
@ -58,90 +57,54 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ range .CheckGroups }} {{ range $checks := .Checks }}
{{ $currentGroup := . }} <tr>
<tr class="row-special"> <th scope="row">
<th scope="rowgroup"> {{ .Name }}
{{ . }}
</th> </th>
<td></td> <td>
<td></td> <span
<td></td> class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
<td></td> >3</span
<td></td> >
<td></td> </td>
</tr> <td>
{{ range $group, $checks := $.Checks }} {{ range .WorkerGroups }}
{{ if eq $group $currentGroup }} <span
{{ range $checks }} class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
<tr> >
<th scope="row" aria-hidden="true">└─</th> {{ . }}
<th scope="row"> </span>
{{ .Name }}
</th>
<td>
{{ if eq .Visibility "PUBLIC" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
Public
</span>
{{ else if eq .Visibility "PRIVATE" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-fuchsia-100 text-fuchsia-800"
>
Private
</span>
{{ else }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
Unknown
</span>
{{ end }}
</td>
<td>
{{ range .WorkerGroups }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ . }}
</span>
{{ end }}
</td>
<td>
{{ if eq .State "ACTIVE" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>
ACTIVE
</span>
{{ else if eq .State "PAUSED" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
>
PAUSED
</span>
{{ else }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
UNKNOWN
</span>
{{ end }}
</td>
<td>
{{ .Schedule }}
</td>
<td>
<a href="/settings/checks/{{ .Id }}" class="link"
>Details</a
>
</td>
</tr>
{{ end }} {{ end }}
{{ end }} </td>
{{ end }} <td>
{{ if eq .State "ACTIVE" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>
ACTIVE
</span>
{{ else if eq .State "PAUSED" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
>
PAUSED
</span>
{{ else }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
UNKNOWN
</span>
{{ end }}
</td>
<td>
{{ .Schedule }}
</td>
<td>
<a href="/settings/checks/{{ .Id }}" class="link">Details</a>
</td>
</tr>
{{ end }} {{ end }}
</tbody> </tbody>
</table> </table>

View file

@ -2,40 +2,20 @@
<section class="p-5"> <section class="p-5">
<form action="/settings/checks/create" method="post"> <form action="/settings/checks/create" method="post">
<label for="name">Name</label> <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> <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> <label for="workergroups">Worker Groups</label>
<input <input
type="text" type="text"
name="workergroups" name="workergroups"
id="workergroups" id="workergroups"
placeholder="NA EU" placeholder="europe asia"
required 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> <label for="schedule">Schedule</label>
<input <input
type="text" type="text"
@ -54,15 +34,27 @@
<code>@daily</code>, <code>@weekly</code>, <code>@monthly</code>, <code>@daily</code>, <code>@weekly</code>, <code>@monthly</code>,
<code>@yearly</code>. <code>@yearly</code>.
</p> </p>
<label for="script">Script</label> <label for="filter">Filter</label>
<textarea required id="script" name="script" class="h-96"> <textarea required id="filter" name="filter" class="sm:col-span-2 h-12">
{{ ScriptUnescapeString .Example }}</textarea {{ ScriptUnescapeString .ExampleFilter }}</textarea
> >
<div <div
id="editor" id="editor-filter"
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden" class="hidden sm:col-span-2 block w-full h-12 rounded-lg border border-gray-300 overflow-hidden"
></div> ></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 Script is what determines the status of a service. You can read more
about it on about it on
<a target="_blank" href="https://k6.io/docs/using-k6/http-requests/" <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 src="/static/monaco/vs/loader.js"></script>
<script> <script>
function htmlDecode(input) { const items = [
var doc = new DOMParser().parseFromString(input, "text/html"); {
return doc.documentElement.textContent; name: "filter",
} language: "javascript",
script = htmlDecode("{{ .Example }}"); options: {
quickSuggestions: false,
document.getElementById("editor").classList.remove("hidden"); },
document.getElementById("script").hidden = true; },
{ name: "script", language: "javascript" },
];
function save() { function save() {
const script = window.editor.getValue(); for (const { name } of items) {
document.getElementById("script").value = script; const elem = window.editors[name].getValue();
document.getElementById(name).value = elem;
}
} }
require.config({ paths: { vs: "/static/monaco/vs" } }); window.editors = {};
require(["vs/editor/editor.main"], function () { for (const { name, language, options = {} } of items) {
window.editor = monaco.editor.create(document.getElementById("editor"), { const textarea = document.getElementById(name);
value: script, const editor = document.getElementById("editor-" + name);
language: "javascript",
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
const divElem = document.getElementById("editor"); editor.classList.remove("hidden");
const resizeObserver = new ResizeObserver((entries) => { textarea.hidden = true;
window.editor.layout();
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 resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(editor);
}); });
resizeObserver.observe(divElem); }
});
</script> </script>
{{ end }} {{ end }}

View file

@ -2,38 +2,6 @@
<section class="p-5"> <section class="p-5">
<form action="/settings/checks/{{ .Check.Id }}" method="post"> <form action="/settings/checks/{{ .Check.Id }}" method="post">
<h2>Configuration</h2> <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> <label for="workergroups">Worker Groups</label>
<input <input
type="text" type="text"
@ -60,15 +28,27 @@
<code>@daily</code>, <code>@weekly</code>, <code>@monthly</code>, <code>@daily</code>, <code>@weekly</code>, <code>@monthly</code>,
<code>@yearly</code>. <code>@yearly</code>.
</p> </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> <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 {{ ScriptUnescapeString .Check.Script }}</textarea
> >
<div <div
id="editor" id="editor-script"
class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden hidden" class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div> ></div>
<p> <p class="sm:col-span-2">
Script is what determines the status of a service. You can read more Script is what determines the status of a service. You can read more
about it on about it on
<a target="_blank" href="https://k6.io/docs/using-k6/http-requests/" <a target="_blank" href="https://k6.io/docs/using-k6/http-requests/"
@ -135,79 +115,119 @@
</caption> </caption>
<thead> <thead>
<tr> <tr>
<th>Check ID</th>
<th>Status</th> <th>Status</th>
<th>Worker Group</th> <th>Worker Group</th>
<th>Created At</th> <th>Started At</th>
<th>Ended At</th>
<th>Duration</th> <th>Duration</th>
<th>Note</th> <th>Note</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ range .History }} {{ range .History }}
<tr> {{ if eq .Status "Running" }}
<td> <tr>
<span <td>{{ .CheckId }}</td>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }} <td>
bg-green-100 text-green-800 <span
{{ else }} class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
bg-red-100 text-red-800 >
{{ end }}" {{ .Status }}...
> </span>
{{ .Status }} </td>
</span> <td>
</td> <span
<td> class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
<span >
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800" {{ .WorkerGroupName }}
> </span>
{{ .WorkerGroupName }} </td>
</span> <td>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
</td> <td></td>
<td> <td></td>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }} <td class="whitespace-normal"></td>
</td> </tr>
<td>{ .Duration }</td> {{ else }}
<td class="whitespace-normal"> <tr>
{{ .Note }} <td>{{ .CheckId }}</td>
</td> <td>
</tr> <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 }}
"
>
{{ .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>{{ .EndTime.Format "2006-01-02 15:04:05" }}</td>
<td>{{ DurationRoundMillisecond .Duration }}</td>
<td class="whitespace-normal">{{ .Note }}</td>
</tr>
{{ end }}
{{ end }} {{ end }}
</tbody> </tbody>
</table> </table>
</section> </section>
<script src="/static/monaco/vs/loader.js"></script> <script src="/static/monaco/vs/loader.js"></script>
<script> <script>
document.getElementById("editor").classList.remove("hidden"); const items = [
document.getElementById("script").hidden = true; {
name: "filter",
language: "javascript",
options: {
quickSuggestions: false,
},
},
{ name: "script", language: "javascript" },
];
function save() { function save() {
const script = window.editor.getValue(); for (const { name } of items) {
document.getElementById('script').value = script; const elem = window.editors[name].getValue();
} document.getElementById(name).value = elem;
}
}
function htmlDecode(input) { window.editors = {};
var doc = new DOMParser().parseFromString(input, "text/html"); for (const { name, language, options = {} } of items) {
return doc.documentElement.textContent; const textarea = document.getElementById(name);
} const editor = document.getElementById("editor-" + name);
script = htmlDecode("{{ .Check.Script }}")
require.config({ paths: { vs: '/static/monaco/vs' } }); editor.classList.remove("hidden");
require(['vs/editor/editor.main'], function () { textarea.hidden = true;
window.editor = monaco.editor.create(document.getElementById('editor'), {
value: script,
language: 'javascript',
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
const divElem = document.getElementById('editor'); require.config({ paths: { vs: "/static/monaco/vs" } });
const resizeObserver = new ResizeObserver(entries => { require(["vs/editor/editor.main"], function () {
window.editor.layout(); window.editors[name] = monaco.editor.create(editor, {
}); value: textarea.value,
resizeObserver.observe(divElem); language: language,
}); minimap: { enabled: false },
</script> codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
wordWrap: "on",
...options,
});
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(editor);
});
}
</script>
{{ end }} {{ end }}

View file

@ -21,9 +21,7 @@
<div <div
class="inline-block bg-white rounded-lg shadow p-5 text-center sm:text-left" 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"> <h3 class="text-sm leading-6 font-medium text-gray-400">Total Checks</h3>
Total Checks
</h3>
<p class="text-3xl font-bold text-black">{{ .ChecksCount }}</p> <p class="text-3xl font-bold text-black">{{ .ChecksCount }}</p>
</div> </div>
<div <div
@ -44,56 +42,76 @@
</div> </div>
</div> </div>
<section class="mt-4"> <section>
<table> <table>
<caption> <caption>
Execution History History
<p>Last 10 executions for all checks and worker groups.</p> <p>Last 10 executions.</p>
</caption> </caption>
<thead> <thead>
<tr> <tr>
<th>Check</th> <th>Check ID</th>
<th>Worker Group</th>
<th>Status</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> <th>Note</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ range .History }} {{ range .History }}
<tr> {{ if eq .Status "Running" }}
<th> <tr>
<a <td>{{ .CheckId }}</td>
class="underline hover:text-blue-600" <td>
href="/settings/checks/{{ .CheckId }}" <span
>{{ .CheckName }}</a class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
> >
</th> {{ .Status }}...
<td> </span>
<span </td>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800" <td>
> <span
{{ .WorkerGroupName }} class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
</span> >
</td> {{ .WorkerGroupName }}
<td> </span>
<span </td>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }} <td>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
bg-green-100 text-green-800 <td></td>
{{ else }} <td></td>
bg-red-100 text-red-800 <td class="whitespace-normal"></td>
{{ end }}" </tr>
> {{ else }}
{{ .Status }} <tr>
</span> <td>{{ .CheckId }}</td>
</td> <td>
<td> <span
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }} class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
</td> {{ if eq .Status "Completed" }}
<td class="whitespace-normal"> bg-purple-100 text-purple-800
{{ .Note }} {{ else }}
</td> bg-red-100 text-red-800
</tr> {{ 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>{{ .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 }} {{ end }}
</tbody> </tbody>
</table> </table>

View file

@ -55,32 +55,88 @@
</a> </a>
</div> </div>
</caption> </caption>
<thead> <thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr> <tr>
<th>Name</th> <th scope="col">Target Group</th>
<th>Type</th> <th scope="col">Name</th>
<th>Action</th> <th scope="col">Visibility</th>
<th scope="col">State</th>
<th scope="col">Action</th>
</tr> </tr>
</thead> </thead>
{{ range .Targets }} <tbody>
<tbody> {{ range .TargetGroups }}
<tr> {{ $currentGroup := . }}
<th scope="row"> <tr class="row-special">
{{ .Name }} <th scope="rowgroup">
{{ . }}
</th> </th>
<td> <td></td>
<span <td></td>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800" <td></td>
> <td></td>
{{ .Type }}
</span>
</td>
<td>
<a href="/settings/targets/{{ .Id }}" class="link">Details</a>
</td>
</tr> </tr>
</tbody> {{ range $group, $targets := $.Targets }}
{{ end }} {{ if eq $group $currentGroup }}
{{ range $targets }}
<tr>
<th scope="row" aria-hidden="true">└─</th>
<th scope="row">
{{ .Name }}
</th>
<td>
{{ if eq .Visibility "PUBLIC" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
Public
</span>
{{ else if eq .Visibility "PRIVATE" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-fuchsia-100 text-fuchsia-800"
>
Private
</span>
{{ else }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
Unknown
</span>
{{ end }}
</td>
<td>
{{ if eq .State "ACTIVE" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>
ACTIVE
</span>
{{ else if eq .State "PAUSED" }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
>
PAUSED
</span>
{{ else }}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
UNKNOWN
</span>
{{ end }}
</td>
<td>
<a href="/settings/targets/{{ .Id }}" class="link"
>Details</a
>
</td>
</tr>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
</tbody>
</table> </table>
</section> </section>
{{ end }} {{ 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> <p>Name of the trigger can be anything.</p>
<label for="script">Script</label> <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 {{ ScriptUnescapeString .Example }}</textarea
> >
<div <div
id="editor" id="editor-script"
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden" class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div> ></div>
<p> <p class="sm:col-span-2">
The trigger script executes for every matching <code>target</code>'s The trigger script executes for every matching <code>target</code>'s
execution of <code>trigger</code>. The outcome of that execution of <code>trigger</code>. The outcome of that
<code>trigger</code> is passed to the script as a <code>trigger</code> is passed to the script as a
@ -30,36 +30,40 @@
<script src="/static/monaco/vs/loader.js"></script> <script src="/static/monaco/vs/loader.js"></script>
<script> <script>
function htmlDecode(input) { const items = [{ name: "script", language: "javascript" }];
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;
function save() { function save() {
const script = window.editor.getValue(); for (const { name } of items) {
document.getElementById("script").value = script; const elem = window.editors[name].getValue();
document.getElementById(name).value = elem;
}
} }
require.config({ paths: { vs: "/static/monaco/vs" } }); window.editors = {};
require(["vs/editor/editor.main"], function () { for (const { name, language, options = {} } of items) {
window.editor = monaco.editor.create(document.getElementById("editor"), { const textarea = document.getElementById(name);
value: script, const editor = document.getElementById("editor-" + name);
language: "javascript",
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
const divElem = document.getElementById("editor"); editor.classList.remove("hidden");
const resizeObserver = new ResizeObserver((entries) => { textarea.hidden = true;
window.editor.layout();
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);
}); });
resizeObserver.observe(divElem); }
});
</script> </script>
{{ end }} {{ end }}

View file

@ -3,14 +3,14 @@
<form action="/settings/triggers/{{ .Trigger.Id }}" method="post"> <form action="/settings/triggers/{{ .Trigger.Id }}" method="post">
<h2>Configuration</h2> <h2>Configuration</h2>
<label for="script">Script</label> <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 {{ ScriptUnescapeString .Trigger.Script }}</textarea
> >
<div <div
id="editor" id="editor-script"
class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden hidden" class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
></div> ></div>
<p> <p class="sm:col-span-2">
The trigger script executes for every matching <code>target</code>'s The trigger script executes for every matching <code>target</code>'s
execution of <code>trigger</code>. The outcome of that execution of <code>trigger</code>. The outcome of that
<code>trigger</code> is passed to the script as a <code>trigger</code> is passed to the script as a
@ -111,37 +111,41 @@
</section> </section>
<script src="/static/monaco/vs/loader.js"></script> <script src="/static/monaco/vs/loader.js"></script>
<script> <script>
document.getElementById("editor").classList.remove("hidden"); const items = [{ name: "script", language: "javascript" }];
document.getElementById("script").hidden = true;
function save() { function save() {
const script = window.editor.getValue(); for (const { name } of items) {
document.getElementById('script').value = script; const elem = window.editors[name].getValue();
} document.getElementById(name).value = elem;
}
}
function htmlDecode(input) { window.editors = {};
var doc = new DOMParser().parseFromString(input, "text/html"); for (const { name, language, options = {} } of items) {
return doc.documentElement.textContent; const textarea = document.getElementById(name);
} const editor = document.getElementById("editor-" + name);
script = htmlDecode("{{ .Trigger.Script }}")
require.config({ paths: { vs: '/static/monaco/vs' } }); editor.classList.remove("hidden");
require(['vs/editor/editor.main'], function () { textarea.hidden = true;
window.editor = monaco.editor.create(document.getElementById('editor'), {
value: script,
language: 'javascript',
minimap: { enabled: false },
codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
});
const divElem = document.getElementById('editor'); require.config({ paths: { vs: "/static/monaco/vs" } });
const resizeObserver = new ResizeObserver(entries => { require(["vs/editor/editor.main"], function () {
window.editor.layout(); window.editors[name] = monaco.editor.create(editor, {
}); value: textarea.value,
resizeObserver.observe(divElem); language: language,
}); minimap: { enabled: false },
</script> codeLens: false,
contextmenu: false,
scrollBeyondLastLine: false,
...options,
});
const resizeObserver = new ResizeObserver((entries) => {
window.editors[name].layout();
});
resizeObserver.observe(editor);
});
}
</script>
{{ end }} {{ end }}

View file

@ -8,8 +8,8 @@ import (
"text/template" "text/template"
"time" "time"
"code.tjo.space/mentos1386/zdravko/internal/script"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/mentos1386/zdravko/pkg/script"
) )
//go:embed * //go:embed *
@ -26,6 +26,12 @@ func load(files ...string) *template.Template {
t := template.New("default").Funcs( t := template.New("default").Funcs(
template.FuncMap{ 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, "StringsJoin": strings.Join,
"Now": time.Now, "Now": time.Now,
"ScriptUnescapeString": script.UnescapeString, "ScriptUnescapeString": script.UnescapeString,
@ -51,6 +57,8 @@ func NewTemplates() *Templates {
"settings_triggers_create.tmpl": loadSettings("pages/settings_triggers_create.tmpl"), "settings_triggers_create.tmpl": loadSettings("pages/settings_triggers_create.tmpl"),
"settings_triggers_describe.tmpl": loadSettings("pages/settings_triggers_describe.tmpl"), "settings_triggers_describe.tmpl": loadSettings("pages/settings_triggers_describe.tmpl"),
"settings_targets.tmpl": loadSettings("pages/settings_targets.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_incidents.tmpl": loadSettings("pages/settings_incidents.tmpl"),
"settings_notifications.tmpl": loadSettings("pages/settings_notifications.tmpl"), "settings_notifications.tmpl": loadSettings("pages/settings_notifications.tmpl"),
"settings_worker_groups.tmpl": loadSettings("pages/settings_worker_groups.tmpl"), "settings_worker_groups.tmpl": loadSettings("pages/settings_worker_groups.tmpl"),