diff --git a/README.md b/README.md index 2fd2cd0..b674086 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ Golang selfhosted Status/Healthcheck monitoring app. -Mostly just a project to test [temporal.io](https://temporal.io/). - ### Roadmap - [x] SSO Support for authentication. - [x] SQLite for database. @@ -37,9 +35,12 @@ Demo is available at https://zdravko.mnts.dev. ```sh # Configure +# You will need to configure an SSO provider +# This can be github for example. cp example.env .env # Generate JWT key +# Copy the values to your .env just generate-jwt-key # Start development environment diff --git a/cmd/zdravko/main.go b/cmd/zdravko/main.go index 793cc30..e29491d 100644 --- a/cmd/zdravko/main.go +++ b/cmd/zdravko/main.go @@ -8,10 +8,10 @@ import ( "sync" "syscall" - "code.tjo.space/mentos1386/zdravko/internal/config" - "code.tjo.space/mentos1386/zdravko/pkg/server" - "code.tjo.space/mentos1386/zdravko/pkg/temporal" - "code.tjo.space/mentos1386/zdravko/pkg/worker" + "github.com/mentos1386/zdravko/internal/config" + "github.com/mentos1386/zdravko/pkg/server" + "github.com/mentos1386/zdravko/pkg/temporal" + "github.com/mentos1386/zdravko/pkg/worker" ) type StartableAndStoppable interface { diff --git a/internal/kv/badger.go b/database/badger.go similarity index 99% rename from internal/kv/badger.go rename to database/badger.go index 8329b34..bc4fefb 100644 --- a/internal/kv/badger.go +++ b/database/badger.go @@ -1,4 +1,4 @@ -package kv +package database import ( "time" diff --git a/internal/kv/kv.go b/database/kv.go similarity index 93% rename from internal/kv/kv.go rename to database/kv.go index 822775e..bfcc20e 100644 --- a/internal/kv/kv.go +++ b/database/kv.go @@ -1,4 +1,4 @@ -package kv +package database import "time" diff --git a/database/models/models.go b/database/models/models.go index 49c5b42..4de1c50 100644 --- a/database/models/models.go +++ b/database/models/models.go @@ -45,15 +45,6 @@ type OAuth2State struct { ExpiresAt *Time `db:"expires_at"` } -type CheckStatus string - -const ( - CheckStatusSuccess CheckStatus = "SUCCESS" - CheckStatusFailure CheckStatus = "FAILURE" - CheckStatusError CheckStatus = "ERROR" - CheckStatusUnknown CheckStatus = "UNKNOWN" -) - type CheckState string const ( @@ -62,45 +53,24 @@ const ( CheckStateUnknown CheckState = "UNKNOWN" ) -type CheckVisibility string - -const ( - CheckVisibilityPublic CheckVisibility = "PUBLIC" - CheckVisibilityPrivate CheckVisibility = "PRIVATE" - CheckVisibilityUnknown CheckVisibility = "UNKNOWN" -) - type Check struct { CreatedAt *Time `db:"created_at"` UpdatedAt *Time `db:"updated_at"` - Id string `db:"id"` - Name string `db:"name"` - Group string `db:"group"` - Visibility CheckVisibility `db:"visibility"` + Id string `db:"id"` + Name string `db:"name"` Schedule string `db:"schedule"` Script string `db:"script"` + Filter string `db:"filter"` } type CheckWithWorkerGroups struct { Check - // List of worker group names WorkerGroups []string } -type CheckHistory struct { - CreatedAt *Time `db:"created_at"` - - CheckId string `db:"check_id"` - Status CheckStatus `db:"status"` - Note string `db:"note"` - - WorkerGroupId string `db:"worker_group_id"` - WorkerGroupName string `db:"worker_group_name"` -} - type WorkerGroup struct { CreatedAt *Time `db:"created_at"` UpdatedAt *Time `db:"updated_at"` @@ -116,15 +86,6 @@ type WorkerGroupWithChecks struct { Checks []string } -type TriggerStatus string - -const ( - TriggerStatusSuccess TriggerStatus = "SUCCESS" - TriggerStatusFailure TriggerStatus = "FAILURE" - TriggerStatusError TriggerStatus = "ERROR" - TriggerStatusUnknown TriggerStatus = "UNKNOWN" -) - type TriggerState string const ( @@ -133,14 +94,6 @@ const ( TriggerStateUnknown TriggerState = "UNKNOWN" ) -type TriggerVisibility string - -const ( - TriggerVisibilityPublic TriggerVisibility = "PUBLIC" - TriggerVisibilityPrivate TriggerVisibility = "PRIVATE" - TriggerVisibilityUnknown TriggerVisibility = "UNKNOWN" -) - type Trigger struct { CreatedAt *Time `db:"created_at"` UpdatedAt *Time `db:"updated_at"` @@ -150,10 +103,46 @@ type Trigger struct { Script string `db:"script"` } -type TriggerHistory struct { +type TargetVisibility string + +const ( + TargetVisibilityPublic TargetVisibility = "PUBLIC" + TargetVisibilityPrivate TargetVisibility = "PRIVATE" + TargetVisibilityUnknown TargetVisibility = "UNKNOWN" +) + +type TargetState string + +const ( + TargetStateActive TargetState = "ACTIVE" + TargetStatePaused TargetState = "PAUSED" + TargetStateUnknown TargetState = "UNKNOWN" +) + +type Target struct { + CreatedAt *Time `db:"created_at"` + UpdatedAt *Time `db:"updated_at"` + + Id string `db:"id"` + Name string `db:"name"` + Group string `db:"group"` + Visibility TargetVisibility `db:"visibility"` + State TargetState `db:"state"` + Metadata string `db:"metadata"` +} + +type TargetStatus string + +const ( + TargetStatusSuccess TargetStatus = "SUCCESS" + TargetStatusFailure TargetStatus = "FAILURE" + TargetStatusUnknown TargetStatus = "UNKNOWN" +) + +type TargetHistory struct { CreatedAt *Time `db:"created_at"` - TriggerId string `db:"trigger_id"` - Status TriggerStatus `db:"status"` - Note string `db:"note"` + TargetId string `db:"target_id"` + Status TargetStatus `db:"status"` + Note string `db:"note"` } diff --git a/database/sqlite/migrations/2024-02-27-initial.sql b/database/sqlite/migrations/2024-02-27-initial.sql index 6ff7afc..eeded5d 100644 --- a/database/sqlite/migrations/2024-02-27-initial.sql +++ b/database/sqlite/migrations/2024-02-27-initial.sql @@ -9,11 +9,10 @@ CREATE TABLE oauth2_states ( CREATE TABLE checks ( id TEXT NOT NULL, name TEXT NOT NULL, - "group" TEXT NOT NULL, schedule TEXT NOT NULL, script TEXT NOT NULL, - visibility TEXT NOT NULL, + filter TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), @@ -21,8 +20,6 @@ CREATE TABLE checks ( PRIMARY KEY (id), CONSTRAINT unique_checks_name UNIQUE (name) ) STRICT; - - -- +migrate StatementBegin CREATE TRIGGER checks_updated_timestamp AFTER UPDATE ON checks BEGIN UPDATE checks SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = NEW.id; @@ -39,7 +36,6 @@ CREATE TABLE worker_groups ( PRIMARY KEY (id), CONSTRAINT unique_worker_groups_name UNIQUE (name) ) STRICT; - -- +migrate StatementBegin CREATE TRIGGER worker_groups_updated_timestamp AFTER UPDATE ON worker_groups BEGIN UPDATE worker_groups SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = NEW.id; @@ -55,19 +51,6 @@ CREATE TABLE check_worker_groups ( CONSTRAINT fk_check_worker_groups_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE ) STRICT; -CREATE TABLE check_histories ( - check_id TEXT NOT NULL, - worker_group_id TEXT NOT NULL, - - status TEXT NOT NULL, - note TEXT NOT NULL, - - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), - - PRIMARY KEY (check_id, worker_group_id, created_at), - CONSTRAINT fk_check_histories_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE, - CONSTRAINT fk_check_histories_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE -) STRICT; CREATE TABLE triggers ( id TEXT NOT NULL, @@ -80,24 +63,44 @@ CREATE TABLE triggers ( PRIMARY KEY (id), CONSTRAINT unique_triggers_name UNIQUE (name) ) STRICT; - - -- +migrate StatementBegin CREATE TRIGGER triggers_updated_timestamp AFTER UPDATE ON triggers BEGIN UPDATE triggers SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = NEW.id; END; -- +migrate StatementEnd -CREATE TABLE trigger_histories ( - trigger_id TEXT NOT NULL, +CREATE TABLE targets ( + id TEXT NOT NULL, + name TEXT NOT NULL, + "group" TEXT NOT NULL, + + visibility TEXT NOT NULL, + state TEXT NOT NULL, + + metadata TEXT NOT NULL, + + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), + + PRIMARY KEY (id), + CONSTRAINT unique_targets_name UNIQUE (name) +) STRICT; +-- +migrate StatementBegin +CREATE TRIGGER targets_updated_timestamp AFTER UPDATE ON targets BEGIN + UPDATE targets SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = NEW.id; +END; +-- +migrate StatementEnd + +CREATE TABLE target_histories ( + target_id TEXT NOT NULL, status TEXT NOT NULL, note TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), - PRIMARY KEY (trigger_id, created_at), - CONSTRAINT fk_trigger_histories_trigger FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE + PRIMARY KEY (target_id, created_at), + CONSTRAINT fk_target_histories_target FOREIGN KEY (target_id) REFERENCES targets(id) ON DELETE CASCADE ) STRICT; -- +migrate Down @@ -105,9 +108,7 @@ DROP TABLE oauth2_states; DROP TABLE check_worker_groups; DROP TABLE worker_groups; DROP TRIGGER worker_groups_updated_timestamp; -DROP TABLE check_histories; DROP TABLE checks; DROP TRIGGER checks_updated_timestamp; DROP TABLE triggers; -DROP TABLE trigger_histories; DROP TRIGGER triggers_updated_timestamp; diff --git a/go.mod b/go.mod index d1040f1..9a9e8dd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module code.tjo.space/mentos1386/zdravko +module github.com/mentos1386/zdravko go 1.21.6 diff --git a/internal/activities/activities.go b/internal/activities/activities.go deleted file mode 100644 index 12d8eeb..0000000 --- a/internal/activities/activities.go +++ /dev/null @@ -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} -} diff --git a/internal/activities/check.go b/internal/activities/check.go deleted file mode 100644 index dd579e5..0000000 --- a/internal/activities/check.go +++ /dev/null @@ -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 -} diff --git a/internal/handlers/api.go b/internal/handlers/api.go deleted file mode 100644 index ce4a573..0000000 --- a/internal/handlers/api.go +++ /dev/null @@ -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"}) -} diff --git a/internal/handlers/settings_targets.go b/internal/handlers/settings_targets.go deleted file mode 100644 index 072d849..0000000 --- a/internal/handlers/settings_targets.go +++ /dev/null @@ -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, - }) -} diff --git a/internal/server/activities/activities.go b/internal/server/activities/activities.go new file mode 100644 index 0000000..bbc2203 --- /dev/null +++ b/internal/server/activities/activities.go @@ -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} +} diff --git a/internal/server/activities/process_check_outcome.go b/internal/server/activities/process_check_outcome.go new file mode 100644 index 0000000..652c3ef --- /dev/null +++ b/internal/server/activities/process_check_outcome.go @@ -0,0 +1,11 @@ +package activities + +import ( + "context" + + "github.com/mentos1386/zdravko/internal/temporal" +) + +func (a *Activities) ProcessCheckOutcome(ctx context.Context, param temporal.ActivityProcessCheckOutcomeParam) (*temporal.ActivityProcessCheckOutcomeResult, error) { + return nil, nil +} diff --git a/internal/server/activities/targets_filter.go b/internal/server/activities/targets_filter.go new file mode 100644 index 0000000..2870b52 --- /dev/null +++ b/internal/server/activities/targets_filter.go @@ -0,0 +1,31 @@ +package activities + +import ( + "context" + + "github.com/mentos1386/zdravko/database/models" + "github.com/mentos1386/zdravko/internal/server/services" +) + +type TargetsFilterParam struct { + Filter string +} + +type TargetsFilterResult struct { + Targets []*models.Target +} + +const TargetsFilterName = "TARGETS_FILTER" + +func (a *Activities) TargetsFilter(ctx context.Context, param TargetsFilterParam) (*TargetsFilterResult, error) { + a.logger.Info("TargetsFilter", "filter", param.Filter) + // TODO: Parse filter. + targets, err := services.GetTargetsWithFilter(ctx, a.db, param.Filter) + if err != nil { + return nil, err + } + + return &TargetsFilterResult{ + Targets: targets, + }, nil +} diff --git a/internal/handlers/404.go b/internal/server/handlers/404.go similarity index 79% rename from internal/handlers/404.go rename to internal/server/handlers/404.go index c558797..f77787b 100644 --- a/internal/handlers/404.go +++ b/internal/server/handlers/404.go @@ -3,7 +3,7 @@ package handlers import ( "net/http" - "code.tjo.space/mentos1386/zdravko/web/templates/components" + "github.com/mentos1386/zdravko/web/templates/components" "github.com/labstack/echo/v4" ) diff --git a/internal/server/handlers/api.go b/internal/server/handlers/api.go new file mode 100644 index 0000000..cc5c286 --- /dev/null +++ b/internal/server/handlers/api.go @@ -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) +} diff --git a/internal/handlers/authentication.go b/internal/server/handlers/authentication.go similarity index 98% rename from internal/handlers/authentication.go rename to internal/server/handlers/authentication.go index 626bc5b..3ef1513 100644 --- a/internal/handlers/authentication.go +++ b/internal/server/handlers/authentication.go @@ -7,8 +7,8 @@ import ( "strings" "time" - jwtInternal "code.tjo.space/mentos1386/zdravko/internal/jwt" "github.com/labstack/echo/v4" + jwtInternal "github.com/mentos1386/zdravko/pkg/jwt" ) const sessionName = "zdravko-hey" diff --git a/internal/handlers/examples.yaml b/internal/server/handlers/examples.yaml similarity index 70% rename from internal/handlers/examples.yaml rename to internal/server/handlers/examples.yaml index b10e289..e7ebea9 100644 --- a/internal/handlers/examples.yaml +++ b/internal/server/handlers/examples.yaml @@ -1,12 +1,8 @@ # Example trigger code trigger: | - import kv from 'zdravko/kv'; - import incidents, { severity } from 'zdravko/incidents'; - - // Only execute on this specific targets. - export function filter(target) { - return target.tags.kind === 'http'; - } + import kv from 'k6/x/zdravko/kv'; + import incidents, { severity } from 'k6/x/zdravko/incidents'; + import { getTarget, getMonitor, getOutcome } from 'k6/x/zdravko'; const getMinute = (date) => { return Math.floor(date.getTime() / 1000 / 60); @@ -25,7 +21,11 @@ trigger: | // This trigger will check if there were more than 5 issues in last // 5 minutes, if so it will create a critical incident. - export default function (target, monitor, outcome) { + export default function () { + const target = getTarget(); + const monitor = getMonitor(); + const outcome = getOutcome(); + // If the outcome is not failure, we close any potential incidents. if (outcome.status !== 'FAILURE') { incidents.close(target, monitor); @@ -62,6 +62,7 @@ trigger: | # Example monitor code check: | import http from 'k6/http'; + import { getTarget } from 'k6/x/zdravko'; export const options = { thresholds: { @@ -70,12 +71,24 @@ check: | }, }; - // Filter out only HTTP targets. - export function filter(target) { - return target.tags.kind === "http"; - }; - // Execute the check on the targets. - export default function (target) { - http.get(target.url); + export default function () { + const { name, group, metadata } = getTarget(); + + console.log(`Running check for ${group}/${name}`) + + http.get(metadata.spec.url); } + +filter: | + kind="Http" and metadata.spec.url!="" + +target: | + kind: Http + tags: + production: "true" + spec: + url: "https://test.k6.io" + method: "GET" + headers: + User-Agent: "Zdravko" diff --git a/internal/handlers/handlers.go b/internal/server/handlers/handlers.go similarity index 69% rename from internal/handlers/handlers.go rename to internal/server/handlers/handlers.go index 97dc252..c0fe09a 100644 --- a/internal/handlers/handlers.go +++ b/internal/server/handlers/handlers.go @@ -4,12 +4,12 @@ import ( "embed" "log/slog" - "code.tjo.space/mentos1386/zdravko/internal/config" - "code.tjo.space/mentos1386/zdravko/internal/kv" - "code.tjo.space/mentos1386/zdravko/internal/script" - "code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/gorilla/sessions" "github.com/jmoiron/sqlx" + "github.com/mentos1386/zdravko/database" + "github.com/mentos1386/zdravko/internal/config" + "github.com/mentos1386/zdravko/pkg/script" + "github.com/mentos1386/zdravko/web/templates/components" "go.temporal.io/sdk/client" "gopkg.in/yaml.v2" ) @@ -18,8 +18,10 @@ import ( var examplesYaml embed.FS type examples struct { - Check string `yaml:"check"` + Check string `yaml:"check"` + Filter string `yaml:"filter"` Trigger string `yaml:"trigger"` + Target string `yaml:"target"` } var Pages = []*components.Page{ @@ -39,7 +41,7 @@ func GetPageByTitle(pages []*components.Page, title string) *components.Page { type BaseHandler struct { db *sqlx.DB - kvStore kv.KeyValueStore + kvStore database.KeyValueStore config *config.ServerConfig logger *slog.Logger @@ -50,7 +52,7 @@ type BaseHandler struct { examples examples } -func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Client, config *config.ServerConfig, logger *slog.Logger) *BaseHandler { +func NewBaseHandler(db *sqlx.DB, kvStore database.KeyValueStore, temporal client.Client, config *config.ServerConfig, logger *slog.Logger) *BaseHandler { store := sessions.NewCookieStore([]byte(config.SessionSecret)) examples := examples{} @@ -64,7 +66,9 @@ func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Clien } examples.Check = script.EscapeString(examples.Check) + examples.Filter = script.EscapeString(examples.Filter) examples.Trigger = script.EscapeString(examples.Trigger) + examples.Target = script.EscapeString(examples.Target) return &BaseHandler{ db: db, diff --git a/internal/handlers/incidents.go b/internal/server/handlers/incidents.go similarity index 81% rename from internal/handlers/incidents.go rename to internal/server/handlers/incidents.go index 9cbcf5e..ffad0c6 100644 --- a/internal/handlers/incidents.go +++ b/internal/server/handlers/incidents.go @@ -3,7 +3,7 @@ package handlers import ( "net/http" - "code.tjo.space/mentos1386/zdravko/web/templates/components" + "github.com/mentos1386/zdravko/web/templates/components" "github.com/labstack/echo/v4" ) diff --git a/internal/handlers/index.go b/internal/server/handlers/index.go similarity index 54% rename from internal/handlers/index.go rename to internal/server/handlers/index.go index 8743a2a..33fb000 100644 --- a/internal/handlers/index.go +++ b/internal/server/handlers/index.go @@ -5,30 +5,30 @@ import ( "net/http" "time" - "code.tjo.space/mentos1386/zdravko/database/models" - "code.tjo.space/mentos1386/zdravko/internal/services" - "code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/labstack/echo/v4" + "github.com/mentos1386/zdravko/database/models" + "github.com/mentos1386/zdravko/internal/server/services" + "github.com/mentos1386/zdravko/web/templates/components" ) type IndexData struct { *components.Base - Checks map[string]ChecksAndStatus - ChecksLength int - TimeRange string - Status models.CheckStatus + Targets map[string]TargetsAndStatus + TargetsLength int + TimeRange string + Status models.TargetStatus } -type Check struct { +type Target struct { Name string - Visibility models.CheckVisibility + Visibility models.TargetVisibility Group string - Status models.CheckStatus + Status models.TargetStatus History *History } type HistoryItem struct { - Status models.CheckStatus + Status models.TargetStatus Date time.Time } @@ -37,23 +37,23 @@ type History struct { Uptime float64 } -type ChecksAndStatus struct { - Status models.CheckStatus - Checks []*Check +type TargetsAndStatus struct { + Status models.TargetStatus + Targets []*Target } func getDateString(date time.Time) string { return date.UTC().Format("2006-01-02T15:04:05") } -func getHistory(history []*models.CheckHistory, period time.Duration, buckets int) *History { - historyMap := map[string]models.CheckStatus{} +func getHistory(history []*services.TargetHistory, period time.Duration, buckets int) *History { + historyMap := map[string]models.TargetStatus{} numOfSuccess := 0.0 numTotal := 0.0 for i := 0; i < buckets; i++ { dateString := getDateString(time.Now().Add(period * time.Duration(-i)).Truncate(period)) - historyMap[dateString] = models.CheckStatusUnknown + historyMap[dateString] = models.TargetStatusUnknown } for _, _history := range history { @@ -65,16 +65,16 @@ func getHistory(history []*models.CheckHistory, period time.Duration, buckets in } numTotal++ - if _history.Status == models.CheckStatusSuccess { + if _history.Status == models.TargetStatusSuccess { numOfSuccess++ } // skip if it is already set to failure - if historyMap[dateString] == models.CheckStatusFailure { + if historyMap[dateString] == models.TargetStatusFailure { continue } - // FIXME: This is wrong! As we can have multiple checks in dateString. + // FIXME: This is wrong! As we can have multiple targets in dateString. // We should look at only the newest one. historyMap[dateString] = _history.Status } @@ -102,7 +102,7 @@ func getHistory(history []*models.CheckHistory, period time.Duration, buckets in func (h *BaseHandler) Index(c echo.Context) error { ctx := context.Background() - checks, err := services.GetChecks(ctx, h.db) + targets, err := services.GetTargets(ctx, h.db) if err != nil { return err } @@ -112,12 +112,12 @@ func (h *BaseHandler) Index(c echo.Context) error { timeRange = "90days" } - overallStatus := models.CheckStatusUnknown - statusByGroup := make(map[string]models.CheckStatus) + overallStatus := models.TargetStatusUnknown + statusByGroup := make(map[string]models.TargetStatus) - checksWithHistory := make([]*Check, len(checks)) - for i, check := range checks { - history, err := services.GetCheckHistoryForCheck(ctx, h.db, check.Id) + targetsWithHistory := make([]*Target, len(targets)) + for i, target := range targets { + history, err := services.GetTargetHistoryForTarget(ctx, h.db, target.Id) if err != nil { return err } @@ -132,38 +132,38 @@ func (h *BaseHandler) Index(c echo.Context) error { historyResult = getHistory(history, time.Minute, 90) } - if statusByGroup[check.Group] == "" { - statusByGroup[check.Group] = models.CheckStatusUnknown + if statusByGroup[target.Group] == "" { + statusByGroup[target.Group] = models.TargetStatusUnknown } status := historyResult.List[len(historyResult.List)-1] - if status.Status == models.CheckStatusSuccess { - if overallStatus == models.CheckStatusUnknown { + if status.Status == models.TargetStatusSuccess { + if overallStatus == models.TargetStatusUnknown { overallStatus = status.Status } - if statusByGroup[check.Group] == models.CheckStatusUnknown { - statusByGroup[check.Group] = status.Status + if statusByGroup[target.Group] == models.TargetStatusUnknown { + statusByGroup[target.Group] = status.Status } } - if status.Status != models.CheckStatusSuccess && status.Status != models.CheckStatusUnknown { + if status.Status != models.TargetStatusSuccess && status.Status != models.TargetStatusUnknown { overallStatus = status.Status - statusByGroup[check.Group] = status.Status + statusByGroup[target.Group] = status.Status } - checksWithHistory[i] = &Check{ - Name: check.Name, - Visibility: check.Visibility, - Group: check.Group, + targetsWithHistory[i] = &Target{ + Name: target.Name, + Visibility: target.Visibility, + Group: target.Group, Status: status.Status, History: historyResult, } } - checksByGroup := map[string]ChecksAndStatus{} - for _, check := range checksWithHistory { - checksByGroup[check.Group] = ChecksAndStatus{ - Status: statusByGroup[check.Group], - Checks: append(checksByGroup[check.Group].Checks, check), + targetsByGroup := map[string]TargetsAndStatus{} + for _, target := range targetsWithHistory { + targetsByGroup[target.Group] = TargetsAndStatus{ + Status: statusByGroup[target.Group], + Targets: append(targetsByGroup[target.Group].Targets, target), } } @@ -174,7 +174,7 @@ func (h *BaseHandler) Index(c echo.Context) error { NavbarActive: GetPageByTitle(Pages, "Status"), Navbar: Pages, }, - Checks: checksByGroup, + Targets: targetsByGroup, TimeRange: timeRange, Status: overallStatus, }) diff --git a/internal/handlers/oauth2.go b/internal/server/handlers/oauth2.go similarity index 96% rename from internal/handlers/oauth2.go rename to internal/server/handlers/oauth2.go index c423a20..e170654 100644 --- a/internal/handlers/oauth2.go +++ b/internal/server/handlers/oauth2.go @@ -12,10 +12,10 @@ import ( "strconv" "time" - "code.tjo.space/mentos1386/zdravko/database/models" - "code.tjo.space/mentos1386/zdravko/internal/config" - "code.tjo.space/mentos1386/zdravko/internal/services" "github.com/labstack/echo/v4" + "github.com/mentos1386/zdravko/database/models" + "github.com/mentos1386/zdravko/internal/config" + "github.com/mentos1386/zdravko/internal/server/services" "golang.org/x/oauth2" ) diff --git a/internal/handlers/settings.go b/internal/server/handlers/settings.go similarity index 94% rename from internal/handlers/settings.go rename to internal/server/handlers/settings.go index f80e867..39d0781 100644 --- a/internal/handlers/settings.go +++ b/internal/server/handlers/settings.go @@ -3,9 +3,9 @@ package handlers import ( "net/http" - "code.tjo.space/mentos1386/zdravko/internal/services" - "code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/labstack/echo/v4" + "github.com/mentos1386/zdravko/internal/server/services" + "github.com/mentos1386/zdravko/web/templates/components" ) type SettingsSidebarGroup struct { @@ -115,7 +115,7 @@ type SettingsHome struct { WorkerGroupsCount int ChecksCount int NotificationsCount int - History []*services.CheckHistoryWithCheck + History []*services.CheckHistory } func (h *BaseHandler) SettingsHomeGET(c echo.Context) error { @@ -132,7 +132,7 @@ func (h *BaseHandler) SettingsHomeGET(c echo.Context) error { return err } - history, err := services.GetLastNCheckHistory(ctx, h.db, 10) + history, err := services.GetLastNCheckHistory(ctx, h.temporal, 10) if err != nil { return err } diff --git a/internal/handlers/settingschecks.go b/internal/server/handlers/settings_checks.go similarity index 83% rename from internal/handlers/settingschecks.go rename to internal/server/handlers/settings_checks.go index 341e19f..65b317c 100644 --- a/internal/handlers/settingschecks.go +++ b/internal/server/handlers/settings_checks.go @@ -5,33 +5,31 @@ import ( "database/sql" "fmt" "net/http" - "slices" "strings" + "time" - "code.tjo.space/mentos1386/zdravko/database/models" - "code.tjo.space/mentos1386/zdravko/internal/script" - "code.tjo.space/mentos1386/zdravko/internal/services" - "code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/go-playground/validator/v10" "github.com/gosimple/slug" "github.com/labstack/echo/v4" + "github.com/mentos1386/zdravko/database/models" + "github.com/mentos1386/zdravko/internal/server/services" + "github.com/mentos1386/zdravko/pkg/script" + "github.com/mentos1386/zdravko/web/templates/components" ) type CreateCheck struct { Name string `validate:"required"` - Group string `validate:"required"` WorkerGroups string `validate:"required"` Schedule string `validate:"required,cron"` Script string `validate:"required"` - Visibility string `validate:"required,oneof=PUBLIC PRIVATE"` + Filter string `validate:"required"` } type UpdateCheck struct { - Group string `validate:"required"` WorkerGroups string `validate:"required"` Schedule string `validate:"required,cron"` Script string `validate:"required"` - Visibility string `validate:"required,oneof=PUBLIC PRIVATE"` + Filter string `validate:"required"` } type CheckWithWorkerGroupsAndState struct { @@ -41,19 +39,24 @@ type CheckWithWorkerGroupsAndState struct { type SettingsChecks struct { *Settings - Checks map[string][]*CheckWithWorkerGroupsAndState - CheckGroups []string + Checks []*CheckWithWorkerGroupsAndState + History []struct { + CreatedAt time.Time + Status string + Note string + } } type SettingsCheck struct { *Settings Check *CheckWithWorkerGroupsAndState - History []*models.CheckHistory + History []*services.CheckHistory } type SettingsCheckCreate struct { *Settings - Example string + ExampleScript string + ExampleFilter string } func (h *BaseHandler) SettingsChecksGET(c echo.Context) error { @@ -76,23 +79,13 @@ func (h *BaseHandler) SettingsChecksGET(c echo.Context) error { } } - checkGroups := []string{} - checksByGroup := map[string][]*CheckWithWorkerGroupsAndState{} - for _, check := range checksWithState { - checksByGroup[check.Group] = append(checksByGroup[check.Group], check) - if !slices.Contains(checkGroups, check.Group) { - checkGroups = append(checkGroups, check.Group) - } - } - return c.Render(http.StatusOK, "settings_checks.tmpl", &SettingsChecks{ Settings: NewSettings( cc.Principal.User, GetPageByTitle(SettingsPages, "Checks"), []*components.Page{GetPageByTitle(SettingsPages, "Checks")}, ), - Checks: checksByGroup, - CheckGroups: checkGroups, + Checks: checksWithState, }) } @@ -116,7 +109,7 @@ func (h *BaseHandler) SettingsChecksDescribeGET(c echo.Context) error { State: status, } - history, err := services.GetCheckHistoryForCheck(context.Background(), h.db, slug) + history, err := services.GetCheckHistoryForCheck(context.Background(), h.temporal, slug) if err != nil { return err } @@ -196,11 +189,10 @@ func (h *BaseHandler) SettingsChecksDescribePOST(c echo.Context) error { checkId := c.Param("id") update := UpdateCheck{ - Group: strings.ToLower(c.FormValue("group")), WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))), Schedule: c.FormValue("schedule"), Script: script.EscapeString(c.FormValue("script")), - Visibility: c.FormValue("visibility"), + Filter: c.FormValue("filter"), } err := validator.New(validator.WithRequiredStructEnabled()).Struct(update) if err != nil { @@ -211,10 +203,9 @@ func (h *BaseHandler) SettingsChecksDescribePOST(c echo.Context) error { if err != nil { return err } - check.Group = update.Group check.Schedule = update.Schedule check.Script = update.Script - check.Visibility = models.CheckVisibility(update.Visibility) + check.Filter = update.Filter err = services.UpdateCheck( ctx, @@ -270,7 +261,8 @@ func (h *BaseHandler) SettingsChecksCreateGET(c echo.Context) error { GetPageByTitle(SettingsPages, "Checks Create"), }, ), - Example: h.examples.Check, + ExampleScript: h.examples.Check, + ExampleFilter: h.examples.Filter, }) } @@ -280,11 +272,10 @@ func (h *BaseHandler) SettingsChecksCreatePOST(c echo.Context) error { create := CreateCheck{ Name: c.FormValue("name"), - Group: strings.ToLower(c.FormValue("group")), WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))), Schedule: c.FormValue("schedule"), Script: script.EscapeString(c.FormValue("script")), - Visibility: c.FormValue("visibility"), + Filter: c.FormValue("filter"), } err := validator.New(validator.WithRequiredStructEnabled()).Struct(create) if err != nil { @@ -312,12 +303,11 @@ func (h *BaseHandler) SettingsChecksCreatePOST(c echo.Context) error { } check := &models.Check{ - Name: create.Name, - Group: create.Group, - Id: checkId, - Schedule: create.Schedule, - Script: create.Script, - Visibility: models.CheckVisibility(create.Visibility), + Name: create.Name, + Id: checkId, + Schedule: create.Schedule, + Script: create.Script, + Filter: create.Filter, } err = services.CreateCheck( diff --git a/internal/handlers/settings_incidents.go b/internal/server/handlers/settings_incidents.go similarity index 90% rename from internal/handlers/settings_incidents.go rename to internal/server/handlers/settings_incidents.go index 856b318..0ff9837 100644 --- a/internal/handlers/settings_incidents.go +++ b/internal/server/handlers/settings_incidents.go @@ -3,7 +3,7 @@ package handlers import ( "net/http" - "code.tjo.space/mentos1386/zdravko/web/templates/components" + "github.com/mentos1386/zdravko/web/templates/components" "github.com/labstack/echo/v4" ) diff --git a/internal/handlers/settings_notifications.go b/internal/server/handlers/settings_notifications.go similarity index 91% rename from internal/handlers/settings_notifications.go rename to internal/server/handlers/settings_notifications.go index 2caa45d..7e5eb56 100644 --- a/internal/handlers/settings_notifications.go +++ b/internal/server/handlers/settings_notifications.go @@ -3,7 +3,7 @@ package handlers import ( "net/http" - "code.tjo.space/mentos1386/zdravko/web/templates/components" + "github.com/mentos1386/zdravko/web/templates/components" "github.com/labstack/echo/v4" ) diff --git a/internal/server/handlers/settings_targets.go b/internal/server/handlers/settings_targets.go new file mode 100644 index 0000000..fbac171 --- /dev/null +++ b/internal/server/handlers/settings_targets.go @@ -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") +} diff --git a/internal/handlers/settings_triggers.go b/internal/server/handlers/settings_triggers.go similarity index 94% rename from internal/handlers/settings_triggers.go rename to internal/server/handlers/settings_triggers.go index faf1fb6..3ba1fa3 100644 --- a/internal/handlers/settings_triggers.go +++ b/internal/server/handlers/settings_triggers.go @@ -5,13 +5,13 @@ import ( "fmt" "net/http" - "code.tjo.space/mentos1386/zdravko/database/models" - "code.tjo.space/mentos1386/zdravko/internal/script" - "code.tjo.space/mentos1386/zdravko/internal/services" - "code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/go-playground/validator/v10" "github.com/gosimple/slug" "github.com/labstack/echo/v4" + "github.com/mentos1386/zdravko/database/models" + "github.com/mentos1386/zdravko/internal/server/services" + "github.com/mentos1386/zdravko/pkg/script" + "github.com/mentos1386/zdravko/web/templates/components" ) type CreateTrigger struct { @@ -36,7 +36,7 @@ type SettingsTriggers struct { type SettingsTrigger struct { *Settings Trigger *TriggerWithState - History []*models.TriggerHistory + History []*services.TriggerHistory } type SettingsTriggerCreate struct { diff --git a/internal/handlers/settingsworkergroups.go b/internal/server/handlers/settings_worker_groups.go similarity index 93% rename from internal/handlers/settingsworkergroups.go rename to internal/server/handlers/settings_worker_groups.go index 5028c2c..d3aecb9 100644 --- a/internal/handlers/settingsworkergroups.go +++ b/internal/server/handlers/settings_worker_groups.go @@ -6,13 +6,13 @@ import ( "net/http" "strings" - "code.tjo.space/mentos1386/zdravko/database/models" - "code.tjo.space/mentos1386/zdravko/internal/jwt" - "code.tjo.space/mentos1386/zdravko/internal/services" - "code.tjo.space/mentos1386/zdravko/web/templates/components" "github.com/go-playground/validator/v10" "github.com/gosimple/slug" "github.com/labstack/echo/v4" + "github.com/mentos1386/zdravko/database/models" + "github.com/mentos1386/zdravko/internal/server/services" + "github.com/mentos1386/zdravko/pkg/jwt" + "github.com/mentos1386/zdravko/web/templates/components" ) type WorkerWithTokenAndActiveWorkers struct { @@ -52,7 +52,7 @@ func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error { } workerGroupsWithActiveWorkers[i] = &WorkerGroupWithActiveWorkers{ WorkerGroupWithChecks: workerGroup, - ActiveWorkers: activeWorkers, + ActiveWorkers: activeWorkers, } } diff --git a/internal/handlers/temporal.go b/internal/server/handlers/temporal.go similarity index 93% rename from internal/handlers/temporal.go rename to internal/server/handlers/temporal.go index 3aa1e59..241eeda 100644 --- a/internal/handlers/temporal.go +++ b/internal/server/handlers/temporal.go @@ -5,8 +5,8 @@ import ( "net/http/httputil" "net/url" - "code.tjo.space/mentos1386/zdravko/internal/jwt" "github.com/labstack/echo/v4" + "github.com/mentos1386/zdravko/pkg/jwt" ) func (h *BaseHandler) Temporal(c echo.Context) error { diff --git a/internal/services/check.go b/internal/server/services/check.go similarity index 91% rename from internal/services/check.go rename to internal/server/services/check.go index 09a8116..67c7473 100644 --- a/internal/services/check.go +++ b/internal/server/services/check.go @@ -6,9 +6,9 @@ import ( "sort" "time" - "code.tjo.space/mentos1386/zdravko/database/models" - "code.tjo.space/mentos1386/zdravko/internal/workflows" "github.com/jmoiron/sqlx" + "github.com/mentos1386/zdravko/database/models" + internaltemporal "github.com/mentos1386/zdravko/internal/temporal" "go.temporal.io/sdk/client" "go.temporal.io/sdk/temporal" "golang.org/x/exp/maps" @@ -55,8 +55,8 @@ func SetCheckState(ctx context.Context, temporal client.Client, id string, state func CreateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error { _, err := db.NamedExecContext(ctx, - `INSERT INTO checks (id, name, visibility, "group", script, schedule) - VALUES (:id, :name, :visibility, :group, :script, :schedule)`, + `INSERT INTO checks (id, name, script, schedule, filter) + VALUES (:id, :name, :script, :schedule, :filter)`, check, ) return err @@ -64,7 +64,7 @@ func CreateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error { func UpdateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error { _, err := db.NamedExecContext(ctx, - `UPDATE checks SET visibility=:visibility, "group"=:group, script=:script, schedule=:schedule WHERE id=:id`, + `UPDATE checks SET script=:script, schedule=:schedule, filter=:filter WHERE id=:id`, check, ) return err @@ -120,12 +120,11 @@ func GetCheckWithWorkerGroups(ctx context.Context, db *sqlx.DB, id string) (*mod SELECT checks.id, checks.name, - checks.visibility, - checks."group", checks.script, checks.schedule, checks.created_at, checks.updated_at, + checks.filter, worker_groups.name as worker_group_name FROM checks LEFT OUTER JOIN check_worker_groups ON checks.id = check_worker_groups.check_id @@ -147,12 +146,11 @@ ORDER BY checks.name err = rows.Scan( &check.Id, &check.Name, - &check.Visibility, - &check.Group, &check.Script, &check.Schedule, &check.CreatedAt, &check.UpdatedAt, + &check.Filter, &workerGroupName, ) if err != nil { @@ -180,12 +178,11 @@ func GetChecksWithWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.Chec SELECT checks.id, checks.name, - checks.visibility, - checks."group", checks.script, checks.schedule, checks.created_at, checks.updated_at, + checks.filter, worker_groups.name as worker_group_name FROM checks LEFT OUTER JOIN check_worker_groups ON checks.id = check_worker_groups.check_id @@ -206,12 +203,11 @@ ORDER BY checks.name err = rows.Scan( &check.Id, &check.Name, - &check.Visibility, - &check.Group, &check.Script, &check.Schedule, &check.CreatedAt, &check.UpdatedAt, + &check.Filter, &workerGroupName, ) if err != nil { @@ -254,7 +250,8 @@ func CreateOrUpdateCheckSchedule( } args := make([]interface{}, 1) - args[0] = workflows.CheckWorkflowParam{ + args[0] = internaltemporal.WorkflowCheckParam{ + Filter: check.Filter, Script: check.Script, CheckId: check.Id, WorkerGroupIds: workerGroupStrings, @@ -268,7 +265,7 @@ func CreateOrUpdateCheckSchedule( }, Action: &client.ScheduleWorkflowAction{ ID: getScheduleId(check.Id), - Workflow: workflows.NewWorkflows(nil).CheckWorkflowDefinition, + Workflow: internaltemporal.WorkflowCheckName, Args: args, TaskQueue: "default", RetryPolicy: &temporal.RetryPolicy{ diff --git a/internal/server/services/check_history.go b/internal/server/services/check_history.go new file mode 100644 index 0000000..cbb747b --- /dev/null +++ b/internal/server/services/check_history.go @@ -0,0 +1,67 @@ +package services + +import ( + "context" + "fmt" + "time" + + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/client" +) + +type CheckHistory struct { + CheckId string + Status string + Duration time.Duration +} + +func GetLastNCheckHistory(ctx context.Context, temporal client.Client, n int32) ([]*CheckHistory, error) { + var checkHistory []*CheckHistory + + response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{ + PageSize: n, + }) + if err != nil { + return checkHistory, err + } + + executions := response.GetExecutions() + + for _, execution := range executions { + scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data) + checkId := scheduleId[len("check-"):] + checkHistory = append(checkHistory, &CheckHistory{ + CheckId: checkId, + Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()), + Status: execution.Status.String(), + }) + } + + return checkHistory, nil +} + +func GetCheckHistoryForCheck(ctx context.Context, temporal client.Client, checkId string) ([]*CheckHistory, error) { + var checkHistory []*CheckHistory + + response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{ + PageSize: 10, + Query: fmt.Sprintf(`TemporalScheduledById = "%s"`, getScheduleId(checkId)), + }) + if err != nil { + return checkHistory, err + } + + executions := response.GetExecutions() + + for _, execution := range executions { + scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data) + checkId := scheduleId[len("check-"):] + checkHistory = append(checkHistory, &CheckHistory{ + CheckId: checkId, + Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()), + Status: execution.Status.String(), + }) + } + + return checkHistory, nil +} diff --git a/internal/services/oauth2_state.go b/internal/server/services/oauth2_state.go similarity index 92% rename from internal/services/oauth2_state.go rename to internal/server/services/oauth2_state.go index 32e7f0f..bade1b5 100644 --- a/internal/services/oauth2_state.go +++ b/internal/server/services/oauth2_state.go @@ -3,7 +3,7 @@ package services import ( "context" - "code.tjo.space/mentos1386/zdravko/database/models" + "github.com/mentos1386/zdravko/database/models" "github.com/jmoiron/sqlx" ) diff --git a/internal/server/services/targets.go b/internal/server/services/targets.go new file mode 100644 index 0000000..6befd7e --- /dev/null +++ b/internal/server/services/targets.go @@ -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 +} diff --git a/internal/server/services/targets_history.go b/internal/server/services/targets_history.go new file mode 100644 index 0000000..9f5046e --- /dev/null +++ b/internal/server/services/targets_history.go @@ -0,0 +1,59 @@ +package services + +import ( + "context" + + "github.com/mentos1386/zdravko/database/models" + "github.com/jmoiron/sqlx" +) + +type TargetHistory struct { + *models.TargetHistory + TargetName string `db:"target_name"` +} + +func GetLastNTargetHistory(ctx context.Context, db *sqlx.DB, n int) ([]*TargetHistory, error) { + var targetHistory []*TargetHistory + err := db.SelectContext(ctx, &targetHistory, ` + SELECT + th.*, + t.name AS target_name + FROM target_histories th + LEFT JOIN targets t ON th.target_id = t.id + WHERE th.target_id = $1 + ORDER BY th.created_at DESC + LIMIT $1 + `, n) + return targetHistory, err +} + +func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string) ([]*TargetHistory, error) { + var targetHistory []*TargetHistory + err := db.SelectContext(ctx, &targetHistory, ` + SELECT + th.*, + t.name AS target_name + FROM target_histories th + LEFT JOIN targets t ON th.target_id = t.id + WHERE th.target_id = $1 + ORDER BY th.created_at DESC + `, targetId) + return targetHistory, err +} + +func AddHistoryForTarget(ctx context.Context, db *sqlx.DB, history *models.TargetHistory) error { + _, err := db.NamedExecContext(ctx, + ` +INSERT INTO target_histories ( + target_id, + status, + note +) VALUES ( + :target_id, + :status, + :note +)`, + history, + ) + return err +} diff --git a/internal/services/trigger.go b/internal/server/services/trigger.go similarity index 96% rename from internal/services/trigger.go rename to internal/server/services/trigger.go index 9d9684d..46d4ff3 100644 --- a/internal/services/trigger.go +++ b/internal/server/services/trigger.go @@ -3,7 +3,7 @@ package services import ( "context" - "code.tjo.space/mentos1386/zdravko/database/models" + "github.com/mentos1386/zdravko/database/models" "github.com/jmoiron/sqlx" ) diff --git a/internal/server/services/trigger_history.go b/internal/server/services/trigger_history.go new file mode 100644 index 0000000..471862b --- /dev/null +++ b/internal/server/services/trigger_history.go @@ -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 +} diff --git a/internal/services/worker_group.go b/internal/server/services/worker_group.go similarity index 98% rename from internal/services/worker_group.go rename to internal/server/services/worker_group.go index 089a7a1..e6a41f2 100644 --- a/internal/services/worker_group.go +++ b/internal/server/services/worker_group.go @@ -3,7 +3,7 @@ package services import ( "context" - "code.tjo.space/mentos1386/zdravko/database/models" + "github.com/mentos1386/zdravko/database/models" "github.com/jmoiron/sqlx" "go.temporal.io/api/enums/v1" "go.temporal.io/sdk/client" diff --git a/internal/server/workflows/check.go b/internal/server/workflows/check.go new file mode 100644 index 0000000..cfadf97 --- /dev/null +++ b/internal/server/workflows/check.go @@ -0,0 +1,58 @@ +package workflows + +import ( + "log/slog" + "sort" + "time" + + "github.com/mentos1386/zdravko/internal/temporal" + "github.com/mentos1386/zdravko/pkg/api" + "go.temporal.io/sdk/workflow" +) + +func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal.WorkflowCheckParam) (api.CheckStatus, error) { + workerGroupIds := param.WorkerGroupIds + sort.Strings(workerGroupIds) + + ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + StartToCloseTimeout: 60 * time.Second, + TaskQueue: temporal.TEMPORAL_SERVER_QUEUE, + }) + targetsFilterParam := temporal.ActivityTargetsFilterParam{ + Filter: param.Filter, + } + targetsFilterResult := temporal.ActivityTargetsFilterResult{} + err := workflow.ExecuteActivity(ctx, temporal.ActivityTargetsFilterName, targetsFilterParam).Get(ctx, &targetsFilterResult) + if err != nil { + return api.CheckStatusUnknown, err + } + + for _, target := range targetsFilterResult.Targets { + for _, workerGroupId := range workerGroupIds { + ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + StartToCloseTimeout: 60 * time.Second, + TaskQueue: workerGroupId, + }) + + heatlcheckParam := temporal.ActivityCheckParam{ + Script: param.Script, + Target: target, + } + + var checkResult *temporal.ActivityCheckResult + err := workflow.ExecuteActivity(ctx, temporal.ActivityCheckName, heatlcheckParam).Get(ctx, &checkResult) + if err != nil { + return api.CheckStatusUnknown, err + } + + status := api.CheckStatusFailure + if checkResult.Success { + status = api.CheckStatusSuccess + } + + slog.Info("Check %s status: %s", param.CheckId, status) + } + } + + return api.CheckStatusSuccess, nil +} diff --git a/internal/server/workflows/workflows.go b/internal/server/workflows/workflows.go new file mode 100644 index 0000000..c79e6f3 --- /dev/null +++ b/internal/server/workflows/workflows.go @@ -0,0 +1,9 @@ +package workflows + + +type Workflows struct { +} + +func NewWorkflows() *Workflows { + return &Workflows{} +} diff --git a/internal/services/check_history.go b/internal/services/check_history.go deleted file mode 100644 index bfb02f2..0000000 --- a/internal/services/check_history.go +++ /dev/null @@ -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 -} diff --git a/internal/temporal/activity_check.go b/internal/temporal/activity_check.go new file mode 100644 index 0000000..8bc5353 --- /dev/null +++ b/internal/temporal/activity_check.go @@ -0,0 +1,15 @@ +package temporal + +import "github.com/mentos1386/zdravko/database/models" + +type ActivityCheckParam struct { + Script string + Target *models.Target +} + +type ActivityCheckResult struct { + Success bool + Note string +} + +const ActivityCheckName = "CHECK" diff --git a/internal/temporal/activity_process_check_outcome.go b/internal/temporal/activity_process_check_outcome.go new file mode 100644 index 0000000..aac2a36 --- /dev/null +++ b/internal/temporal/activity_process_check_outcome.go @@ -0,0 +1,10 @@ +package temporal + +type ActivityProcessCheckOutcomeParam struct { + Outcome string +} + +type ActivityProcessCheckOutcomeResult struct { +} + +const ActivityProcessCheckOutcomeName = "PROCESS_CHECK_OUTCOME" diff --git a/internal/temporal/activity_targets_filter.go b/internal/temporal/activity_targets_filter.go new file mode 100644 index 0000000..b204f7e --- /dev/null +++ b/internal/temporal/activity_targets_filter.go @@ -0,0 +1,15 @@ +package temporal + +import ( + "github.com/mentos1386/zdravko/database/models" +) + +type ActivityTargetsFilterParam struct { + Filter string +} + +type ActivityTargetsFilterResult struct { + Targets []*models.Target +} + +const ActivityTargetsFilterName = "TARGETS_FILTER" diff --git a/internal/temporal/temporal.go b/internal/temporal/temporal.go index d6ac701..026e1f0 100644 --- a/internal/temporal/temporal.go +++ b/internal/temporal/temporal.go @@ -5,13 +5,17 @@ import ( "log/slog" "time" - "code.tjo.space/mentos1386/zdravko/internal/config" - "code.tjo.space/mentos1386/zdravko/internal/jwt" - "code.tjo.space/mentos1386/zdravko/pkg/retry" + "github.com/mentos1386/zdravko/internal/config" + "github.com/mentos1386/zdravko/pkg/jwt" + "github.com/mentos1386/zdravko/pkg/retry" "github.com/pkg/errors" "go.temporal.io/sdk/client" ) +// Must be default, as we are also processing +// some temporal things. +const TEMPORAL_SERVER_QUEUE = "default" + type AuthHeadersProvider struct { Token string } diff --git a/internal/temporal/workflow_check.go b/internal/temporal/workflow_check.go new file mode 100644 index 0000000..c4ac178 --- /dev/null +++ b/internal/temporal/workflow_check.go @@ -0,0 +1,10 @@ +package temporal + +type WorkflowCheckParam struct { + Script string + Filter string + CheckId string + WorkerGroupIds []string +} + +const WorkflowCheckName = "CHECK_WORKFLOW" diff --git a/internal/worker/activities/activities.go b/internal/worker/activities/activities.go new file mode 100644 index 0000000..ab11bf8 --- /dev/null +++ b/internal/worker/activities/activities.go @@ -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} +} diff --git a/internal/worker/activities/check.go b/internal/worker/activities/check.go new file mode 100644 index 0000000..3c4ffd4 --- /dev/null +++ b/internal/worker/activities/check.go @@ -0,0 +1,21 @@ +package activities + +import ( + "context" + "log/slog" + + "github.com/mentos1386/zdravko/internal/temporal" + "github.com/mentos1386/zdravko/pkg/k6" + "github.com/mentos1386/zdravko/pkg/script" +) + +func (a *Activities) Check(ctx context.Context, param temporal.ActivityCheckParam) (*temporal.ActivityCheckResult, error) { + execution := k6.NewExecution(slog.Default(), script.UnescapeString(param.Script)) + + result, err := execution.Run(ctx) + if err != nil { + return nil, err + } + + return &temporal.ActivityCheckResult{Success: result.Success, Note: result.Note}, nil +} diff --git a/internal/worker/workflows/workflows.go b/internal/worker/workflows/workflows.go new file mode 100644 index 0000000..40cf0aa --- /dev/null +++ b/internal/worker/workflows/workflows.go @@ -0,0 +1,8 @@ +package workflows + +type Workflows struct { +} + +func NewWorkflows() *Workflows { + return &Workflows{} +} diff --git a/internal/workflows/check.go b/internal/workflows/check.go deleted file mode 100644 index b8a561e..0000000 --- a/internal/workflows/check.go +++ /dev/null @@ -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 -} diff --git a/internal/workflows/workflows.go b/internal/workflows/workflows.go deleted file mode 100644 index 78b7cfe..0000000 --- a/internal/workflows/workflows.go +++ /dev/null @@ -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} -} diff --git a/justfile b/justfile index 63af5ed..e1c2a1b 100644 --- a/justfile +++ b/justfile @@ -43,8 +43,14 @@ test: # Generates new jwt key pair generate-jwt-key: - openssl genrsa -out jwt.private.pem 2048 - openssl rsa -pubout -in jwt.private.pem -out jwt.public.pem + @openssl genrsa -out jwt.private.pem 2048 > /dev/null 2>&1 + @openssl rsa -pubout -in jwt.private.pem -out jwt.public.pem > /dev/null 2>&1 + @echo "Private key:" + @cat jwt.private.pem | sed -z 's/\n/\\n/g' + @echo + @echo "Public key:" + @cat jwt.public.pem | sed -z 's/\n/\\n/g' + @echo # Deploy the application to fly.io deploy-fly: @@ -119,11 +125,12 @@ _monaco-download: mv node_modules/monaco-editor/min {{STATIC_DIR}}/monaco rm -rf node_modules - # We onlt care about javascript language + # We only care about javascript language find {{STATIC_DIR}}/monaco/vs/basic-languages/ \ -type d \ -not -name 'javascript' \ -not -name 'typescript' \ + -not -name 'yaml' \ -not -name 'basic-languages' \ -prune -exec rm -rf {} \; diff --git a/pkg/api/checks.go b/pkg/api/checks.go index dc86cd4..04d35d4 100644 --- a/pkg/api/checks.go +++ b/pkg/api/checks.go @@ -1,9 +1,15 @@ package api -import "code.tjo.space/mentos1386/zdravko/database/models" +type CheckStatus string + +const ( + CheckStatusSuccess CheckStatus = "SUCCESS" + CheckStatusFailure CheckStatus = "FAILURE" + CheckStatusUnknown CheckStatus = "UNKNOWN" +) type ApiV1ChecksHistoryPOSTBody struct { - Status models.CheckStatus `json:"status"` - Note string `json:"note"` - WorkerGroupId string `json:"worker_group"` + Status CheckStatus `json:"status"` + Note string `json:"note"` + WorkerGroupId string `json:"worker_group"` } diff --git a/internal/jwt/jwt.go b/pkg/jwt/jwt.go similarity index 98% rename from internal/jwt/jwt.go rename to pkg/jwt/jwt.go index 2e7ad6d..a9cc26d 100644 --- a/internal/jwt/jwt.go +++ b/pkg/jwt/jwt.go @@ -6,7 +6,7 @@ import ( "encoding/hex" "time" - "code.tjo.space/mentos1386/zdravko/database/models" + "github.com/mentos1386/zdravko/database/models" "github.com/golang-jwt/jwt/v5" "github.com/pkg/errors" ) diff --git a/pkg/k6/k6_test.go b/pkg/k6/k6_test.go index 257081f..2f9deda 100644 --- a/pkg/k6/k6_test.go +++ b/pkg/k6/k6_test.go @@ -5,6 +5,8 @@ import ( "log/slog" "os" "testing" + + "github.com/mentos1386/zdravko/pkg/k6/zdravko" ) func getLogger() *slog.Logger { @@ -18,12 +20,12 @@ func getLogger() *slog.Logger { } func TestK6Success(t *testing.T) { - ctx := context.Background() logger := getLogger() script := ` import http from 'k6/http'; import { sleep } from 'k6'; +import { getTarget } from 'k6/x/zdravko'; export const options = { vus: 10, @@ -31,6 +33,8 @@ export const options = { }; export default function () { + const target = getTarget(); + console.log('Target:', target); http.get('https://test.k6.io'); sleep(1); } @@ -38,6 +42,14 @@ export default function () { execution := NewExecution(logger, script) + ctx := zdravko.WithZdravkoContext(context.Background(), zdravko.Context{Target: zdravko.Target{ + Name: "Test", + Group: "Test", + Metadata: map[string]interface{}{ + "Kind": "Test", + }, + }}) + result, err := execution.Run(ctx) if err != nil { t.Errorf("Error starting execution: %v", err) diff --git a/pkg/k6/zdravko/context.go b/pkg/k6/zdravko/context.go new file mode 100644 index 0000000..e83fc8d --- /dev/null +++ b/pkg/k6/zdravko/context.go @@ -0,0 +1,23 @@ +package zdravko + +import "context" + +type zdravkoContextKey string + +type Target struct { + Name string + Group string + Metadata map[string]interface{} +} + +type Context struct { + Target Target +} + +func WithZdravkoContext(ctx context.Context, zdravkoContext Context) context.Context { + return context.WithValue(ctx, zdravkoContextKey("zdravko-ctx"), zdravkoContext) +} + +func GetZdravkoContext(ctx context.Context) Context { + return ctx.Value(zdravkoContextKey("zdravko-ctx")).(Context) +} diff --git a/pkg/k6/zdravko/zdravko.go b/pkg/k6/zdravko/zdravko.go new file mode 100644 index 0000000..45299c3 --- /dev/null +++ b/pkg/k6/zdravko/zdravko.go @@ -0,0 +1,59 @@ +package zdravko + +import ( + "go.k6.io/k6/js/modules" +) + +func init() { + modules.Register("k6/x/zdravko", New()) +} + +type ( + // RootModule is the global module instance that will create module + // instances for each VU. + RootModule struct{} + + // ModuleInstance represents an instance of the JS module. + ModuleInstance struct { + // vu provides methods for accessing internal k6 objects for a VU + vu modules.VU + // comparator is the exported type + zdravko *Zdravko + } +) + +// Ensure the interfaces are implemented correctly. +var ( + _ modules.Instance = &ModuleInstance{} + _ modules.Module = &RootModule{} +) + +// New returns a pointer to a new RootModule instance. +func New() *RootModule { + return &RootModule{} +} + +// NewModuleInstance implements the modules.Module interface returning a new instance for each VU. +func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance { + return &ModuleInstance{ + vu: vu, + zdravko: &Zdravko{vu: vu}, + } +} + +type Zdravko struct { + vu modules.VU + Targets []string +} + +func (z *Zdravko) GetTarget() Target { + zdravkoContext := GetZdravkoContext(z.vu.Context()) + return zdravkoContext.Target +} + +// Exports implements the modules.Instance interface and returns the exported types for the JS module. +func (mi *ModuleInstance) Exports() modules.Exports { + return modules.Exports{ + Default: mi.zdravko, + } +} diff --git a/internal/script/script.go b/pkg/script/script.go similarity index 100% rename from internal/script/script.go rename to pkg/script/script.go diff --git a/pkg/server/routes.go b/pkg/server/routes.go index feec577..1043238 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -4,20 +4,20 @@ import ( "log/slog" "net/http" - "code.tjo.space/mentos1386/zdravko/internal/config" - "code.tjo.space/mentos1386/zdravko/internal/handlers" - "code.tjo.space/mentos1386/zdravko/internal/kv" - "code.tjo.space/mentos1386/zdravko/web/static" "github.com/jmoiron/sqlx" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/mentos1386/zdravko/database" + "github.com/mentos1386/zdravko/internal/config" + "github.com/mentos1386/zdravko/internal/server/handlers" + "github.com/mentos1386/zdravko/web/static" "go.temporal.io/sdk/client" ) func Routes( e *echo.Echo, sqlDb *sqlx.DB, - kvStore kv.KeyValueStore, + kvStore database.KeyValueStore, temporalClient client.Client, cfg *config.ServerConfig, logger *slog.Logger, @@ -64,13 +64,13 @@ func Routes( settings.GET("/triggers/:id/enable", h.SettingsTriggersEnableGET) settings.GET("/targets", h.SettingsTargetsGET) - //settings.GET("/targets/create", h.SettingsTargetsCreateGET) - //settings.POST("/targets/create", h.SettingsTargetsCreatePOST) - //settings.GET("/targets/:id", h.SettingsTargetsDescribeGET) - //settings.POST("/targets/:id", h.SettingsTargetsDescribePOST) - //settings.GET("/targets/:id/delete", h.SettingsTargetsDescribeDELETE) - //settings.GET("/targets/:id/disable", h.SettingsTargetsDisableGET) - //settings.GET("/targets/:id/enable", h.SettingsTargetsEnableGET) + settings.GET("/targets/create", h.SettingsTargetsCreateGET) + settings.POST("/targets/create", h.SettingsTargetsCreatePOST) + settings.GET("/targets/:id", h.SettingsTargetsDescribeGET) + settings.POST("/targets/:id", h.SettingsTargetsDescribePOST) + settings.GET("/targets/:id/delete", h.SettingsTargetsDescribeDELETE) + settings.GET("/targets/:id/disable", h.SettingsTargetsDisableGET) + settings.GET("/targets/:id/enable", h.SettingsTargetsEnableGET) settings.GET("/incidents", h.SettingsIncidentsGET) @@ -103,7 +103,6 @@ func Routes( apiv1 := e.Group("/api/v1") apiv1.Use(h.Authenticated) apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET) - apiv1.POST("/checks/:id/history", h.ApiV1ChecksHistoryPOST) // Error handler e.HTTPErrorHandler = func(err error, c echo.Context) { diff --git a/pkg/server/server.go b/pkg/server/server.go index d00371d..2753483 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -4,13 +4,12 @@ import ( "context" "log/slog" - "code.tjo.space/mentos1386/zdravko/database" - "code.tjo.space/mentos1386/zdravko/internal/config" - "code.tjo.space/mentos1386/zdravko/internal/kv" - "code.tjo.space/mentos1386/zdravko/internal/temporal" - "code.tjo.space/mentos1386/zdravko/web/templates" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/mentos1386/zdravko/database" + "github.com/mentos1386/zdravko/internal/config" + "github.com/mentos1386/zdravko/internal/temporal" + "github.com/mentos1386/zdravko/web/templates" "github.com/pkg/errors" ) @@ -45,12 +44,12 @@ func (s *Server) Start() error { return errors.Wrap(err, "failed to connect to temporal") } - kvStore, err := kv.NewBadgerKeyValueStore(s.cfg.KeyValueDatabasePath) + kvStore, err := database.NewBadgerKeyValueStore(s.cfg.KeyValueDatabasePath) if err != nil { return errors.Wrap(err, "failed to open kv store") } - s.worker = NewWorker(temporalClient, s.cfg) + s.worker = NewWorker(temporalClient, s.cfg, s.logger, sqliteDb, kvStore) s.echo.Renderer = templates.NewTemplates() s.echo.Use(middleware.Logger()) diff --git a/pkg/server/worker.go b/pkg/server/worker.go index d0963b6..feb6fa6 100644 --- a/pkg/server/worker.go +++ b/pkg/server/worker.go @@ -1,34 +1,45 @@ package server import ( - "code.tjo.space/mentos1386/zdravko/internal/activities" - "code.tjo.space/mentos1386/zdravko/internal/config" - "code.tjo.space/mentos1386/zdravko/internal/workflows" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/mentos1386/zdravko/database" + "github.com/mentos1386/zdravko/internal/config" + "github.com/mentos1386/zdravko/internal/server/activities" + "github.com/mentos1386/zdravko/internal/server/workflows" + "github.com/mentos1386/zdravko/internal/temporal" + "go.temporal.io/sdk/activity" "go.temporal.io/sdk/client" - "go.temporal.io/sdk/worker" + temporalWorker "go.temporal.io/sdk/worker" + "go.temporal.io/sdk/workflow" ) type Worker struct { - worker worker.Worker + worker temporalWorker.Worker } -func NewWorker(temporalClient client.Client, cfg *config.ServerConfig) *Worker { - w := worker.New(temporalClient, "default", worker.Options{}) +func NewWorker(temporalClient client.Client, cfg *config.ServerConfig, logger *slog.Logger, db *sqlx.DB, kvStore database.KeyValueStore) *Worker { + worker := temporalWorker.New(temporalClient, temporal.TEMPORAL_SERVER_QUEUE, temporalWorker.Options{}) - workerActivities := activities.NewActivities(&config.WorkerConfig{}) + a := activities.NewActivities(cfg, logger, db, kvStore) - workerWorkflows := workflows.NewWorkflows(workerActivities) + w := workflows.NewWorkflows() // Register Workflows - w.RegisterWorkflow(workerWorkflows.CheckWorkflowDefinition) + worker.RegisterWorkflowWithOptions(w.CheckWorkflowDefinition, workflow.RegisterOptions{Name: temporal.WorkflowCheckName}) + + // Register Activities + worker.RegisterActivityWithOptions(a.TargetsFilter, activity.RegisterOptions{Name: temporal.ActivityTargetsFilterName}) + worker.RegisterActivityWithOptions(a.ProcessCheckOutcome, activity.RegisterOptions{Name: temporal.ActivityProcessCheckOutcomeName}) return &Worker{ - worker: w, + worker: worker, } } func (w *Worker) Start() error { - return w.worker.Run(worker.InterruptCh()) + return w.worker.Run(temporalWorker.InterruptCh()) } func (w *Worker) Stop() { diff --git a/pkg/temporal/config.go b/pkg/temporal/config.go index 41610f0..ebfcfe8 100644 --- a/pkg/temporal/config.go +++ b/pkg/temporal/config.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - internal "code.tjo.space/mentos1386/zdravko/internal/config" - "code.tjo.space/mentos1386/zdravko/internal/jwt" + internal "github.com/mentos1386/zdravko/internal/config" + "github.com/mentos1386/zdravko/pkg/jwt" "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/config" "go.temporal.io/server/common/persistence/sql/sqlplugin/sqlite" diff --git a/pkg/temporal/server.go b/pkg/temporal/server.go index a92625a..3739225 100644 --- a/pkg/temporal/server.go +++ b/pkg/temporal/server.go @@ -16,7 +16,7 @@ import ( func NewServer(cfg *config.Config, tokenKeyProvider authorization.TokenKeyProvider) (t.Server, error) { logger := log.NewZapLogger(log.BuildZapLogger(log.Config{ Stdout: true, - Level: "info", + Level: "warn", OutputFile: "", })) diff --git a/pkg/temporal/temporal.go b/pkg/temporal/temporal.go index 81706c7..77cacb7 100644 --- a/pkg/temporal/temporal.go +++ b/pkg/temporal/temporal.go @@ -1,7 +1,7 @@ package temporal import ( - "code.tjo.space/mentos1386/zdravko/internal/config" + "github.com/mentos1386/zdravko/internal/config" "github.com/temporalio/ui-server/v2/server" t "go.temporal.io/server/temporal" ) diff --git a/pkg/temporal/ui.go b/pkg/temporal/ui.go index bbc9972..cecdf71 100644 --- a/pkg/temporal/ui.go +++ b/pkg/temporal/ui.go @@ -1,7 +1,7 @@ package temporal import ( - internal "code.tjo.space/mentos1386/zdravko/internal/config" + internal "github.com/mentos1386/zdravko/internal/config" "github.com/temporalio/ui-server/v2/server" "github.com/temporalio/ui-server/v2/server/config" "github.com/temporalio/ui-server/v2/server/server_options" diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go index 81273e2..fc669bb 100644 --- a/pkg/worker/worker.go +++ b/pkg/worker/worker.go @@ -3,17 +3,17 @@ package worker import ( "encoding/json" "io" - "log" + "log/slog" "net/http" "time" - "code.tjo.space/mentos1386/zdravko/internal/activities" - "code.tjo.space/mentos1386/zdravko/internal/config" - "code.tjo.space/mentos1386/zdravko/internal/temporal" - "code.tjo.space/mentos1386/zdravko/internal/workflows" - "code.tjo.space/mentos1386/zdravko/pkg/api" - "code.tjo.space/mentos1386/zdravko/pkg/retry" + "github.com/mentos1386/zdravko/internal/config" + "github.com/mentos1386/zdravko/internal/temporal" + "github.com/mentos1386/zdravko/internal/worker/activities" + "github.com/mentos1386/zdravko/pkg/api" + "github.com/mentos1386/zdravko/pkg/retry" "github.com/pkg/errors" + "go.temporal.io/sdk/activity" "go.temporal.io/sdk/worker" ) @@ -60,11 +60,13 @@ func getConnectionConfig(token string, apiUrl string) (*ConnectionConfig, error) type Worker struct { worker worker.Worker cfg *config.WorkerConfig + logger *slog.Logger } func NewWorker(cfg *config.WorkerConfig) (*Worker, error) { return &Worker{ - cfg: cfg, + cfg: cfg, + logger: slog.Default().WithGroup("worker"), }, nil } @@ -78,7 +80,7 @@ func (w *Worker) Start() error { return err } - log.Println("Worker Group:", config.Group) + w.logger.Info("Worker Starting", "group", config.Group) temporalClient, err := temporal.ConnectWorkerToTemporal(w.cfg.Token, config.Endpoint) if err != nil { @@ -88,15 +90,10 @@ func (w *Worker) Start() error { // Create a new Worker w.worker = worker.New(temporalClient, config.Group, worker.Options{}) - workerActivities := activities.NewActivities(w.cfg) - workerWorkflows := workflows.NewWorkflows(workerActivities) - - // Register Workflows - w.worker.RegisterWorkflow(workerWorkflows.CheckWorkflowDefinition) + workerActivities := activities.NewActivities(w.cfg, w.logger) // Register Activities - w.worker.RegisterActivity(workerActivities.Check) - w.worker.RegisterActivity(workerActivities.CheckAddToHistory) + w.worker.RegisterActivityWithOptions(workerActivities.Check, activity.RegisterOptions{Name: temporal.ActivityCheckName}) return w.worker.Run(worker.InterruptCh()) } diff --git a/web/static/css/main.css b/web/static/css/main.css index 4c62ee2..e4c7811 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -64,11 +64,11 @@ code { @apply bg-blue-700 text-white; } -.checks .time-range > a { +.targets .time-range > a { @apply font-medium text-sm px-2.5 py-1 rounded-lg; @apply text-black bg-gray-100 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400; } -.checks .time-range > a.active { +.targets .time-range > a.active { @apply bg-white hover:bg-gray-300 shadow; } diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index 11709b9..c8319a6 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -722,6 +722,10 @@ video { display: none; } +.h-12 { + height: 3rem; +} + .h-20 { height: 5rem; } @@ -1278,6 +1282,10 @@ video { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + .transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); @@ -1408,7 +1416,7 @@ code { color: rgb(255 255 255 / var(--tw-text-opacity)); } -.checks .time-range > a { +.targets .time-range > a { border-radius: 0.5rem; padding-left: 0.625rem; padding-right: 0.625rem; @@ -1423,12 +1431,12 @@ code { color: rgb(0 0 0 / var(--tw-text-opacity)); } -.checks .time-range > a:hover { +.targets .time-range > a:hover { --tw-bg-opacity: 1; background-color: rgb(209 213 219 / var(--tw-bg-opacity)); } -.checks .time-range > a:focus { +.targets .time-range > a:focus { outline: 2px solid transparent; outline-offset: 2px; --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); @@ -1438,7 +1446,7 @@ code { --tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity)); } -.checks .time-range > a.active { +.targets .time-range > a.active { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); @@ -1446,7 +1454,7 @@ code { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.checks .time-range > a.active:hover { +.targets .time-range > a.active:hover { --tw-bg-opacity: 1; background-color: rgb(209 213 219 / var(--tw-bg-opacity)); } diff --git a/web/static/monaco/vs/basic-languages/yaml/yaml.js b/web/static/monaco/vs/basic-languages/yaml/yaml.js new file mode 100644 index 0000000..b6eba27 --- /dev/null +++ b/web/static/monaco/vs/basic-languages/yaml/yaml.js @@ -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; +}); diff --git a/web/templates/pages/index.tmpl b/web/templates/pages/index.tmpl index b8205b6..288bee0 100644 --- a/web/templates/pages/index.tmpl +++ b/web/templates/pages/index.tmpl @@ -1,26 +1,26 @@ {{ define "main" }}
- {{ $length := len .Checks }} + {{ $length := len .Targets }} {{ if eq $length 0 }}

- There are no checks yet. + There are no targets yet.

- Create a check to check your services and get notified when they are - down. + Create a target to target your services and get notified when they + are down.

{{ end }} -
+
- {{ range $group, $checksAndStatus := .Checks }} + {{ range $group, $targetsAndStatus := .Targets }}
- {{ if eq $checksAndStatus.Status "SUCCESS" }} + {{ if eq $targetsAndStatus.Status "SUCCESS" }} - {{ else if eq $checksAndStatus.Status "FAILURE" }} + {{ else if eq $targetsAndStatus.Status "FAILURE" }} @@ -123,7 +123,7 @@ - {{ range $checksAndStatus.Checks }} + {{ range $targetsAndStatus.Targets }}
diff --git a/web/templates/pages/settings_checks.tmpl b/web/templates/pages/settings_checks.tmpl index da31be3..cea2c9a 100644 --- a/web/templates/pages/settings_checks.tmpl +++ b/web/templates/pages/settings_checks.tmpl @@ -48,9 +48,8 @@ - Check Group Name - Visibility + Targets Worker Groups State Schedule @@ -58,90 +57,54 @@ - {{ range .CheckGroups }} - {{ $currentGroup := . }} - - - {{ . }} + {{ range $checks := .Checks }} + + + {{ .Name }} - - - - - - - - {{ range $group, $checks := $.Checks }} - {{ if eq $group $currentGroup }} - {{ range $checks }} - - └─ - - {{ .Name }} - - - {{ if eq .Visibility "PUBLIC" }} - - Public - - {{ else if eq .Visibility "PRIVATE" }} - - Private - - {{ else }} - - Unknown - - {{ end }} - - - {{ range .WorkerGroups }} - - {{ . }} - - {{ end }} - - - {{ if eq .State "ACTIVE" }} - - ACTIVE - - {{ else if eq .State "PAUSED" }} - - PAUSED - - {{ else }} - - UNKNOWN - - {{ end }} - - - {{ .Schedule }} - - - Details - - + + 3 + + + {{ range .WorkerGroups }} + + {{ . }} + {{ end }} - {{ end }} - {{ end }} + + + {{ if eq .State "ACTIVE" }} + + ACTIVE + + {{ else if eq .State "PAUSED" }} + + PAUSED + + {{ else }} + + UNKNOWN + + {{ end }} + + + {{ .Schedule }} + + + Details + + {{ end }} diff --git a/web/templates/pages/settings_checks_create.tmpl b/web/templates/pages/settings_checks_create.tmpl index c87f139..802dc49 100644 --- a/web/templates/pages/settings_checks_create.tmpl +++ b/web/templates/pages/settings_checks_create.tmpl @@ -2,40 +2,20 @@
- +

Name of the check can be anything.

- - -

- Visibility determines who can see the check. If set to - public, it will be visible to everyone on the homepage. - Otherwise it will be only visible to signed in users. -

- - -

- Group checks together. This affects how they are presented on the - homepage. -

-

Worker groups are used to distribute the check to specific workers.

+

+ Worker groups are used to distribute the check to specific workers. + Space is a separator between groups. +

@daily, @weekly, @monthly, @yearly.

- - Filter + +

+ With filter we specify what targets the check will run on. For whole + grammar on what the filter query can look like, please read the + MQL Grammar + documentation. +

+ + +

@@ -75,36 +73,42 @@ {{ end }} diff --git a/web/templates/pages/settings_checks_describe.tmpl b/web/templates/pages/settings_checks_describe.tmpl index f523183..83e29f3 100644 --- a/web/templates/pages/settings_checks_describe.tmpl +++ b/web/templates/pages/settings_checks_describe.tmpl @@ -2,38 +2,6 @@

Configuration

- - -

- Visibility determines who can see the check. If set to - public, it will be visible to everyone on the homepage. - Otherwise it will be only visible to signed in users. -

- - -

- Group checks together. This affects how they are presented on the - homepage. -

@daily, @weekly, @monthly, @yearly.

+ + + +

+ With filter we specify what targets the check will run on. For whole + grammar on what the filter query can look like, please read the + MQL Grammar + documentation. +

@@ -135,6 +121,7 @@ + Check ID Status Worker Group Created At @@ -144,70 +131,100 @@ {{ range .History }} - - - - {{ .Status }} - - - - - {{ .WorkerGroupName }} - - - - {{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }} - - { .Duration } - - {{ .Note }} - - + {{ if eq .Status "Running" }} + + {{ .CheckId }} + + + {{ .Status }} + + + + + { .WorkerGroupName } + + + { .CreatedAt.Time.Format "2006-01-02 15:04:05" } + / + / + + {{ else }} + + {{ .CheckId }} + + + {{ .Status }} + + + + + { .WorkerGroupName } + + + { .CreatedAt.Time.Format "2006-01-02 15:04:05" } + {{ .Duration }} + { .Note } + + {{ end }} {{ end }}

- + 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); + }); + } + {{ end }} diff --git a/web/templates/pages/settings_home.tmpl b/web/templates/pages/settings_home.tmpl index 5873451..d45b105 100644 --- a/web/templates/pages/settings_home.tmpl +++ b/web/templates/pages/settings_home.tmpl @@ -21,9 +21,7 @@
-

- Total Checks -

+

Total Checks

{{ .ChecksCount }}

{{ .CheckName }}{{ .CheckId }} - {{ .WorkerGroupName }} + { .WorkerGroupName } @@ -87,12 +85,8 @@ {{ .Status }} - - {{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }} - - - {{ .Note }} - + { .CreatedAt.Time.Format "2006-01-02 15:04:05" } + { .Note } {{ end }} diff --git a/web/templates/pages/settings_targets.tmpl b/web/templates/pages/settings_targets.tmpl index 16198fd..949f529 100644 --- a/web/templates/pages/settings_targets.tmpl +++ b/web/templates/pages/settings_targets.tmpl @@ -55,32 +55,88 @@
- + - Name - Type - Action + Target Group + Name + Visibility + State + Action - {{ range .Targets }} - - - - {{ .Name }} + + {{ range .TargetGroups }} + {{ $currentGroup := . }} + + + {{ . }} - - - {{ .Type }} - - - - Details - + + + + - - {{ end }} + {{ range $group, $targets := $.Targets }} + {{ if eq $group $currentGroup }} + {{ range $targets }} + + └─ + + {{ .Name }} + + + {{ if eq .Visibility "PUBLIC" }} + + Public + + {{ else if eq .Visibility "PRIVATE" }} + + Private + + {{ else }} + + Unknown + + {{ end }} + + + {{ if eq .State "ACTIVE" }} + + ACTIVE + + {{ else if eq .State "PAUSED" }} + + PAUSED + + {{ else }} + + UNKNOWN + + {{ end }} + + + Details + + + {{ end }} + {{ end }} + {{ end }} + {{ end }} +
{{ end }} diff --git a/web/templates/pages/settings_targets_create.tmpl b/web/templates/pages/settings_targets_create.tmpl new file mode 100644 index 0000000..9fc1c5b --- /dev/null +++ b/web/templates/pages/settings_targets_create.tmpl @@ -0,0 +1,86 @@ +{{ define "settings" }} +
+ + + +

Name of the target can be anything.

+ + +

+ Visibility determines who can see the target. If set to + public, it will be visible to everyone on the homepage. + Otherwise it will be only visible to signed in users. +

+ + +

+ Group targets together. This affects how they are presented on the + homepage. +

+ + + +

+ Metadata is a YAML object that contains the configuration for the + target. This configuration can be then used for Checks to + filter the targets to act on as well as by using + getTarget() + function to fetch target metadata. +

+ + +
+ + + +{{ end }} diff --git a/web/templates/pages/settings_targets_describe.tmpl b/web/templates/pages/settings_targets_describe.tmpl new file mode 100644 index 0000000..c5e9a2a --- /dev/null +++ b/web/templates/pages/settings_targets_describe.tmpl @@ -0,0 +1,190 @@ +{{ define "settings" }} +
+
+

Configuration

+ + +

+ Visibility determines who can see the target. If set to + public, it will be visible to everyone on the homepage. + Otherwise it will be only visible to signed in users. +

+ + +

+ Group targets together. This affects how they are presented on the + homepage. +

+ + + +

+ Metadata is a YAML object that contains the configuration for the + target. This configuration can be then used for Targets to + filter the targets to act on as well as by using + getTarget() + function to fetch target metadata. +

+ +
+
+ +
+
+

+ State + {{ if eq .Target.State "ACTIVE" }} + + ACTIVE + + {{ else if eq .Target.State "PAUSED" }} + + PAUSED + + {{ end }} +

+

+ 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. +

+ {{ if eq .Target.State "ACTIVE" }} + Pause + {{ else if eq .Target.State "PAUSED" }} + Resume + {{ end }} +
+ +
+

Danger Zone

+

Permanently delete this target.

+ Delete +
+
+ +
+ + + + + + + + + + + + + {{ range .History }} + + + + + + + + {{ end }} + +
+ History +

Last 10 executions of the targets.

+
StatusWorker GroupCreated AtDurationNote
+ + {{ .Status }} + + + + {{ .WorkerGroupName }} + + + {{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }} + { .Duration } + {{ .Note }} +
+
+ + + +{{ end }} diff --git a/web/templates/pages/settings_triggers_create.tmpl b/web/templates/pages/settings_triggers_create.tmpl index 7669009..c67dfdf 100644 --- a/web/templates/pages/settings_triggers_create.tmpl +++ b/web/templates/pages/settings_triggers_create.tmpl @@ -14,7 +14,7 @@ {{ ScriptUnescapeString .Example }}

@@ -30,36 +30,39 @@ {{ end }} diff --git a/web/templates/pages/settings_triggers_describe.tmpl b/web/templates/pages/settings_triggers_describe.tmpl index 95057d0..1b8ef26 100644 --- a/web/templates/pages/settings_triggers_describe.tmpl +++ b/web/templates/pages/settings_triggers_describe.tmpl @@ -7,7 +7,7 @@ {{ ScriptUnescapeString .Trigger.Script }}

@@ -111,37 +111,40 @@

- + 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); + }); + } + {{ end }} diff --git a/web/templates/templates.go b/web/templates/templates.go index 7afd573..39621e0 100644 --- a/web/templates/templates.go +++ b/web/templates/templates.go @@ -8,8 +8,8 @@ import ( "text/template" "time" - "code.tjo.space/mentos1386/zdravko/internal/script" "github.com/labstack/echo/v4" + "github.com/mentos1386/zdravko/pkg/script" ) //go:embed * @@ -51,6 +51,8 @@ func NewTemplates() *Templates { "settings_triggers_create.tmpl": loadSettings("pages/settings_triggers_create.tmpl"), "settings_triggers_describe.tmpl": loadSettings("pages/settings_triggers_describe.tmpl"), "settings_targets.tmpl": loadSettings("pages/settings_targets.tmpl"), + "settings_targets_create.tmpl": loadSettings("pages/settings_targets_create.tmpl"), + "settings_targets_describe.tmpl": loadSettings("pages/settings_targets_describe.tmpl"), "settings_incidents.tmpl": loadSettings("pages/settings_incidents.tmpl"), "settings_notifications.tmpl": loadSettings("pages/settings_notifications.tmpl"), "settings_worker_groups.tmpl": loadSettings("pages/settings_worker_groups.tmpl"),