mirror of
https://github.com/mentos1386/zdravko.git
synced 2024-11-21 23:33:34 +00:00
feat: refactor, renames almost working workflow
This commit is contained in:
parent
73835b500d
commit
0f28cdf175
80 changed files with 1850 additions and 962 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package kv
|
||||
package database
|
||||
|
||||
import (
|
||||
"time"
|
|
@ -1,4 +1,4 @@
|
|||
package kv
|
||||
package database
|
||||
|
||||
import "time"
|
||||
|
|
@ -45,15 +45,6 @@ type OAuth2State struct {
|
|||
ExpiresAt *Time `db:"expires_at"`
|
||||
}
|
||||
|
||||
type CheckStatus string
|
||||
|
||||
const (
|
||||
CheckStatusSuccess CheckStatus = "SUCCESS"
|
||||
CheckStatusFailure CheckStatus = "FAILURE"
|
||||
CheckStatusError CheckStatus = "ERROR"
|
||||
CheckStatusUnknown CheckStatus = "UNKNOWN"
|
||||
)
|
||||
|
||||
type CheckState string
|
||||
|
||||
const (
|
||||
|
@ -62,45 +53,24 @@ const (
|
|||
CheckStateUnknown CheckState = "UNKNOWN"
|
||||
)
|
||||
|
||||
type CheckVisibility string
|
||||
|
||||
const (
|
||||
CheckVisibilityPublic CheckVisibility = "PUBLIC"
|
||||
CheckVisibilityPrivate CheckVisibility = "PRIVATE"
|
||||
CheckVisibilityUnknown CheckVisibility = "UNKNOWN"
|
||||
)
|
||||
|
||||
type Check struct {
|
||||
CreatedAt *Time `db:"created_at"`
|
||||
UpdatedAt *Time `db:"updated_at"`
|
||||
|
||||
Id string `db:"id"`
|
||||
Name string `db:"name"`
|
||||
Group string `db:"group"`
|
||||
Visibility CheckVisibility `db:"visibility"`
|
||||
|
||||
Schedule string `db:"schedule"`
|
||||
Script string `db:"script"`
|
||||
Filter string `db:"filter"`
|
||||
}
|
||||
|
||||
type CheckWithWorkerGroups struct {
|
||||
Check
|
||||
|
||||
// List of worker group names
|
||||
WorkerGroups []string
|
||||
}
|
||||
|
||||
type CheckHistory struct {
|
||||
CreatedAt *Time `db:"created_at"`
|
||||
|
||||
CheckId string `db:"check_id"`
|
||||
Status CheckStatus `db:"status"`
|
||||
Note string `db:"note"`
|
||||
|
||||
WorkerGroupId string `db:"worker_group_id"`
|
||||
WorkerGroupName string `db:"worker_group_name"`
|
||||
}
|
||||
|
||||
type WorkerGroup struct {
|
||||
CreatedAt *Time `db:"created_at"`
|
||||
UpdatedAt *Time `db:"updated_at"`
|
||||
|
@ -116,15 +86,6 @@ type WorkerGroupWithChecks struct {
|
|||
Checks []string
|
||||
}
|
||||
|
||||
type TriggerStatus string
|
||||
|
||||
const (
|
||||
TriggerStatusSuccess TriggerStatus = "SUCCESS"
|
||||
TriggerStatusFailure TriggerStatus = "FAILURE"
|
||||
TriggerStatusError TriggerStatus = "ERROR"
|
||||
TriggerStatusUnknown TriggerStatus = "UNKNOWN"
|
||||
)
|
||||
|
||||
type TriggerState string
|
||||
|
||||
const (
|
||||
|
@ -133,14 +94,6 @@ const (
|
|||
TriggerStateUnknown TriggerState = "UNKNOWN"
|
||||
)
|
||||
|
||||
type TriggerVisibility string
|
||||
|
||||
const (
|
||||
TriggerVisibilityPublic TriggerVisibility = "PUBLIC"
|
||||
TriggerVisibilityPrivate TriggerVisibility = "PRIVATE"
|
||||
TriggerVisibilityUnknown TriggerVisibility = "UNKNOWN"
|
||||
)
|
||||
|
||||
type Trigger struct {
|
||||
CreatedAt *Time `db:"created_at"`
|
||||
UpdatedAt *Time `db:"updated_at"`
|
||||
|
@ -150,10 +103,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"`
|
||||
TargetId string `db:"target_id"`
|
||||
Status TargetStatus `db:"status"`
|
||||
Note string `db:"note"`
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
2
go.mod
2
go.mod
|
@ -1,4 +1,4 @@
|
|||
module code.tjo.space/mentos1386/zdravko
|
||||
module github.com/mentos1386/zdravko
|
||||
|
||||
go 1.21.6
|
||||
|
||||
|
|
|
@ -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}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
20
internal/server/activities/activities.go
Normal file
20
internal/server/activities/activities.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package activities
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mentos1386/zdravko/database"
|
||||
"github.com/mentos1386/zdravko/internal/config"
|
||||
)
|
||||
|
||||
type Activities struct {
|
||||
config *config.ServerConfig
|
||||
db *sqlx.DB
|
||||
kvStore database.KeyValueStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewActivities(config *config.ServerConfig, logger *slog.Logger, db *sqlx.DB, kvStore database.KeyValueStore) *Activities {
|
||||
return &Activities{config: config, logger: logger, db: db, kvStore: kvStore}
|
||||
}
|
11
internal/server/activities/process_check_outcome.go
Normal file
11
internal/server/activities/process_check_outcome.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package activities
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mentos1386/zdravko/internal/temporal"
|
||||
)
|
||||
|
||||
func (a *Activities) ProcessCheckOutcome(ctx context.Context, param temporal.ActivityProcessCheckOutcomeParam) (*temporal.ActivityProcessCheckOutcomeResult, error) {
|
||||
return nil, nil
|
||||
}
|
31
internal/server/activities/targets_filter.go
Normal file
31
internal/server/activities/targets_filter.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
36
internal/server/handlers/api.go
Normal file
36
internal/server/handlers/api.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mentos1386/zdravko/internal/server/services"
|
||||
)
|
||||
|
||||
type ApiV1WorkersConnectGETResponse struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
|
||||
func (h *BaseHandler) ApiV1WorkersConnectGET(c echo.Context) error {
|
||||
ctx := context.Background()
|
||||
cc := c.(AuthenticatedContext)
|
||||
|
||||
workerGroup, err := services.GetWorkerGroup(ctx, h.db, cc.Principal.Worker.Group)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Token invalid")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
response := ApiV1WorkersConnectGETResponse{
|
||||
Endpoint: h.config.Temporal.ServerHost,
|
||||
Group: workerGroup.Id,
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
|
@ -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"
|
|
@ -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"
|
|
@ -4,12 +4,12 @@ import (
|
|||
"embed"
|
||||
"log/slog"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/kv"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/script"
|
||||
"code.tjo.space/mentos1386/zdravko/web/templates/components"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mentos1386/zdravko/database"
|
||||
"github.com/mentos1386/zdravko/internal/config"
|
||||
"github.com/mentos1386/zdravko/pkg/script"
|
||||
"github.com/mentos1386/zdravko/web/templates/components"
|
||||
"go.temporal.io/sdk/client"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
@ -19,7 +19,9 @@ var examplesYaml embed.FS
|
|||
|
||||
type examples struct {
|
||||
Check string `yaml:"check"`
|
||||
Filter string `yaml:"filter"`
|
||||
Trigger string `yaml:"trigger"`
|
||||
Target string `yaml:"target"`
|
||||
}
|
||||
|
||||
var Pages = []*components.Page{
|
||||
|
@ -39,7 +41,7 @@ func GetPageByTitle(pages []*components.Page, title string) *components.Page {
|
|||
|
||||
type BaseHandler struct {
|
||||
db *sqlx.DB
|
||||
kvStore kv.KeyValueStore
|
||||
kvStore database.KeyValueStore
|
||||
config *config.ServerConfig
|
||||
logger *slog.Logger
|
||||
|
||||
|
@ -50,7 +52,7 @@ type BaseHandler struct {
|
|||
examples examples
|
||||
}
|
||||
|
||||
func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Client, config *config.ServerConfig, logger *slog.Logger) *BaseHandler {
|
||||
func NewBaseHandler(db *sqlx.DB, kvStore database.KeyValueStore, temporal client.Client, config *config.ServerConfig, logger *slog.Logger) *BaseHandler {
|
||||
store := sessions.NewCookieStore([]byte(config.SessionSecret))
|
||||
|
||||
examples := examples{}
|
||||
|
@ -64,7 +66,9 @@ func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Clien
|
|||
}
|
||||
|
||||
examples.Check = script.EscapeString(examples.Check)
|
||||
examples.Filter = script.EscapeString(examples.Filter)
|
||||
examples.Trigger = script.EscapeString(examples.Trigger)
|
||||
examples.Target = script.EscapeString(examples.Target)
|
||||
|
||||
return &BaseHandler{
|
||||
db: db,
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -5,30 +5,30 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/database/models"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/services"
|
||||
"code.tjo.space/mentos1386/zdravko/web/templates/components"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mentos1386/zdravko/database/models"
|
||||
"github.com/mentos1386/zdravko/internal/server/services"
|
||||
"github.com/mentos1386/zdravko/web/templates/components"
|
||||
)
|
||||
|
||||
type IndexData struct {
|
||||
*components.Base
|
||||
Checks map[string]ChecksAndStatus
|
||||
ChecksLength int
|
||||
Targets map[string]TargetsAndStatus
|
||||
TargetsLength int
|
||||
TimeRange string
|
||||
Status models.CheckStatus
|
||||
Status models.TargetStatus
|
||||
}
|
||||
|
||||
type Check struct {
|
||||
type Target struct {
|
||||
Name string
|
||||
Visibility models.CheckVisibility
|
||||
Visibility models.TargetVisibility
|
||||
Group string
|
||||
Status models.CheckStatus
|
||||
Status models.TargetStatus
|
||||
History *History
|
||||
}
|
||||
|
||||
type HistoryItem struct {
|
||||
Status models.CheckStatus
|
||||
Status models.TargetStatus
|
||||
Date time.Time
|
||||
}
|
||||
|
||||
|
@ -37,23 +37,23 @@ type History struct {
|
|||
Uptime float64
|
||||
}
|
||||
|
||||
type ChecksAndStatus struct {
|
||||
Status models.CheckStatus
|
||||
Checks []*Check
|
||||
type TargetsAndStatus struct {
|
||||
Status models.TargetStatus
|
||||
Targets []*Target
|
||||
}
|
||||
|
||||
func getDateString(date time.Time) string {
|
||||
return date.UTC().Format("2006-01-02T15:04:05")
|
||||
}
|
||||
|
||||
func getHistory(history []*models.CheckHistory, period time.Duration, buckets int) *History {
|
||||
historyMap := map[string]models.CheckStatus{}
|
||||
func getHistory(history []*services.TargetHistory, period time.Duration, buckets int) *History {
|
||||
historyMap := map[string]models.TargetStatus{}
|
||||
numOfSuccess := 0.0
|
||||
numTotal := 0.0
|
||||
|
||||
for i := 0; i < buckets; i++ {
|
||||
dateString := getDateString(time.Now().Add(period * time.Duration(-i)).Truncate(period))
|
||||
historyMap[dateString] = models.CheckStatusUnknown
|
||||
historyMap[dateString] = models.TargetStatusUnknown
|
||||
}
|
||||
|
||||
for _, _history := range history {
|
||||
|
@ -65,16 +65,16 @@ func getHistory(history []*models.CheckHistory, period time.Duration, buckets in
|
|||
}
|
||||
|
||||
numTotal++
|
||||
if _history.Status == models.CheckStatusSuccess {
|
||||
if _history.Status == models.TargetStatusSuccess {
|
||||
numOfSuccess++
|
||||
}
|
||||
|
||||
// skip if it is already set to failure
|
||||
if historyMap[dateString] == models.CheckStatusFailure {
|
||||
if historyMap[dateString] == models.TargetStatusFailure {
|
||||
continue
|
||||
}
|
||||
|
||||
// FIXME: This is wrong! As we can have multiple checks in dateString.
|
||||
// FIXME: This is wrong! As we can have multiple targets in dateString.
|
||||
// We should look at only the newest one.
|
||||
historyMap[dateString] = _history.Status
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ func getHistory(history []*models.CheckHistory, period time.Duration, buckets in
|
|||
|
||||
func (h *BaseHandler) Index(c echo.Context) error {
|
||||
ctx := context.Background()
|
||||
checks, err := services.GetChecks(ctx, h.db)
|
||||
targets, err := services.GetTargets(ctx, h.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -112,12 +112,12 @@ func (h *BaseHandler) Index(c echo.Context) error {
|
|||
timeRange = "90days"
|
||||
}
|
||||
|
||||
overallStatus := models.CheckStatusUnknown
|
||||
statusByGroup := make(map[string]models.CheckStatus)
|
||||
overallStatus := models.TargetStatusUnknown
|
||||
statusByGroup := make(map[string]models.TargetStatus)
|
||||
|
||||
checksWithHistory := make([]*Check, len(checks))
|
||||
for i, check := range checks {
|
||||
history, err := services.GetCheckHistoryForCheck(ctx, h.db, check.Id)
|
||||
targetsWithHistory := make([]*Target, len(targets))
|
||||
for i, target := range targets {
|
||||
history, err := services.GetTargetHistoryForTarget(ctx, h.db, target.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -132,38 +132,38 @@ func (h *BaseHandler) Index(c echo.Context) error {
|
|||
historyResult = getHistory(history, time.Minute, 90)
|
||||
}
|
||||
|
||||
if statusByGroup[check.Group] == "" {
|
||||
statusByGroup[check.Group] = models.CheckStatusUnknown
|
||||
if statusByGroup[target.Group] == "" {
|
||||
statusByGroup[target.Group] = models.TargetStatusUnknown
|
||||
}
|
||||
|
||||
status := historyResult.List[len(historyResult.List)-1]
|
||||
if status.Status == models.CheckStatusSuccess {
|
||||
if overallStatus == models.CheckStatusUnknown {
|
||||
if status.Status == models.TargetStatusSuccess {
|
||||
if overallStatus == models.TargetStatusUnknown {
|
||||
overallStatus = status.Status
|
||||
}
|
||||
if statusByGroup[check.Group] == models.CheckStatusUnknown {
|
||||
statusByGroup[check.Group] = status.Status
|
||||
if statusByGroup[target.Group] == models.TargetStatusUnknown {
|
||||
statusByGroup[target.Group] = status.Status
|
||||
}
|
||||
}
|
||||
if status.Status != models.CheckStatusSuccess && status.Status != models.CheckStatusUnknown {
|
||||
if status.Status != models.TargetStatusSuccess && status.Status != models.TargetStatusUnknown {
|
||||
overallStatus = status.Status
|
||||
statusByGroup[check.Group] = status.Status
|
||||
statusByGroup[target.Group] = status.Status
|
||||
}
|
||||
|
||||
checksWithHistory[i] = &Check{
|
||||
Name: check.Name,
|
||||
Visibility: check.Visibility,
|
||||
Group: check.Group,
|
||||
targetsWithHistory[i] = &Target{
|
||||
Name: target.Name,
|
||||
Visibility: target.Visibility,
|
||||
Group: target.Group,
|
||||
Status: status.Status,
|
||||
History: historyResult,
|
||||
}
|
||||
}
|
||||
|
||||
checksByGroup := map[string]ChecksAndStatus{}
|
||||
for _, check := range checksWithHistory {
|
||||
checksByGroup[check.Group] = ChecksAndStatus{
|
||||
Status: statusByGroup[check.Group],
|
||||
Checks: append(checksByGroup[check.Group].Checks, check),
|
||||
targetsByGroup := map[string]TargetsAndStatus{}
|
||||
for _, target := range targetsWithHistory {
|
||||
targetsByGroup[target.Group] = TargetsAndStatus{
|
||||
Status: statusByGroup[target.Group],
|
||||
Targets: append(targetsByGroup[target.Group].Targets, target),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ func (h *BaseHandler) Index(c echo.Context) error {
|
|||
NavbarActive: GetPageByTitle(Pages, "Status"),
|
||||
Navbar: Pages,
|
||||
},
|
||||
Checks: checksByGroup,
|
||||
Targets: targetsByGroup,
|
||||
TimeRange: timeRange,
|
||||
Status: overallStatus,
|
||||
})
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
@ -313,11 +304,10 @@ func (h *BaseHandler) SettingsChecksCreatePOST(c echo.Context) error {
|
|||
|
||||
check := &models.Check{
|
||||
Name: create.Name,
|
||||
Group: create.Group,
|
||||
Id: checkId,
|
||||
Schedule: create.Schedule,
|
||||
Script: create.Script,
|
||||
Visibility: models.CheckVisibility(create.Visibility),
|
||||
Filter: create.Filter,
|
||||
}
|
||||
|
||||
err = services.CreateCheck(
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
240
internal/server/handlers/settings_targets.go
Normal file
240
internal/server/handlers/settings_targets.go
Normal file
|
@ -0,0 +1,240 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mentos1386/zdravko/database/models"
|
||||
"github.com/mentos1386/zdravko/internal/server/services"
|
||||
"github.com/mentos1386/zdravko/web/templates/components"
|
||||
)
|
||||
|
||||
type CreateTarget struct {
|
||||
Name string `validate:"required"`
|
||||
Group string `validate:"required"`
|
||||
Visibility string `validate:"required,oneof=PUBLIC PRIVATE"`
|
||||
Metadata string `validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateTarget struct {
|
||||
Group string `validate:"required"`
|
||||
Visibility string `validate:"required,oneof=PUBLIC PRIVATE"`
|
||||
Metadata string `validate:"required"`
|
||||
}
|
||||
|
||||
type SettingsTargets struct {
|
||||
*Settings
|
||||
Targets map[string][]*models.Target
|
||||
TargetGroups []string
|
||||
}
|
||||
|
||||
type SettingsTarget struct {
|
||||
*Settings
|
||||
Target *models.Target
|
||||
History []*services.TargetHistory
|
||||
}
|
||||
|
||||
type SettingsTargetCreate struct {
|
||||
*Settings
|
||||
Example string
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsTargetsGET(c echo.Context) error {
|
||||
cc := c.(AuthenticatedContext)
|
||||
|
||||
targets, err := services.GetTargets(context.Background(), h.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetGroups := []string{}
|
||||
targetsByGroup := map[string][]*models.Target{}
|
||||
for _, target := range targets {
|
||||
targetsByGroup[target.Group] = append(targetsByGroup[target.Group], target)
|
||||
if !slices.Contains(targetGroups, target.Group) {
|
||||
targetGroups = append(targetGroups, target.Group)
|
||||
}
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "settings_targets.tmpl", &SettingsTargets{
|
||||
Settings: NewSettings(
|
||||
cc.Principal.User,
|
||||
GetPageByTitle(SettingsPages, "Targets"),
|
||||
[]*components.Page{GetPageByTitle(SettingsPages, "Targets")},
|
||||
),
|
||||
Targets: targetsByGroup,
|
||||
TargetGroups: targetGroups,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsTargetsDescribeGET(c echo.Context) error {
|
||||
cc := c.(AuthenticatedContext)
|
||||
|
||||
slug := c.Param("id")
|
||||
|
||||
target, err := services.GetTarget(context.Background(), h.db, slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
history, err := services.GetTargetHistoryForTarget(context.Background(), h.db, slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maxElements := 10
|
||||
if len(history) < maxElements {
|
||||
maxElements = len(history)
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "settings_targets_describe.tmpl", &SettingsTarget{
|
||||
Settings: NewSettings(
|
||||
cc.Principal.User,
|
||||
GetPageByTitle(SettingsPages, "Targets"),
|
||||
[]*components.Page{
|
||||
GetPageByTitle(SettingsPages, "Targets"),
|
||||
{
|
||||
Path: fmt.Sprintf("/settings/targets/%s", slug),
|
||||
Title: "Describe",
|
||||
Breadcrumb: target.Name,
|
||||
},
|
||||
}),
|
||||
Target: target,
|
||||
History: history[:maxElements],
|
||||
})
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsTargetsDescribeDELETE(c echo.Context) error {
|
||||
slug := c.Param("id")
|
||||
|
||||
err := services.DeleteTarget(context.Background(), h.db, slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, "/settings/targets")
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsTargetsDisableGET(c echo.Context) error {
|
||||
slug := c.Param("id")
|
||||
|
||||
target, err := services.GetTarget(context.Background(), h.db, slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = services.SetTargetState(context.Background(), h.db, target.Id, models.TargetStatePaused)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/targets/%s", slug))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsTargetsEnableGET(c echo.Context) error {
|
||||
slug := c.Param("id")
|
||||
|
||||
target, err := services.GetTarget(context.Background(), h.db, slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = services.SetTargetState(context.Background(), h.db, target.Id, models.TargetStateActive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/targets/%s", slug))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsTargetsDescribePOST(c echo.Context) error {
|
||||
ctx := context.Background()
|
||||
targetId := c.Param("id")
|
||||
|
||||
update := UpdateTarget{
|
||||
Group: strings.ToLower(c.FormValue("group")),
|
||||
Visibility: c.FormValue("visibility"),
|
||||
Metadata: c.FormValue("metadata"),
|
||||
}
|
||||
err := validator.New(validator.WithRequiredStructEnabled()).Struct(update)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target, err := services.GetTarget(ctx, h.db, targetId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target.Group = update.Group
|
||||
target.Visibility = models.TargetVisibility(update.Visibility)
|
||||
target.Metadata = update.Metadata
|
||||
|
||||
err = services.UpdateTarget(
|
||||
ctx,
|
||||
h.db,
|
||||
target,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/targets/%s", targetId))
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsTargetsCreateGET(c echo.Context) error {
|
||||
cc := c.(AuthenticatedContext)
|
||||
|
||||
return c.Render(http.StatusOK, "settings_targets_create.tmpl", &SettingsTargetCreate{
|
||||
Settings: NewSettings(
|
||||
cc.Principal.User,
|
||||
GetPageByTitle(SettingsPages, "Targets"),
|
||||
[]*components.Page{
|
||||
GetPageByTitle(SettingsPages, "Targets"),
|
||||
GetPageByTitle(SettingsPages, "Targets Create"),
|
||||
},
|
||||
),
|
||||
Example: h.examples.Target,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsTargetsCreatePOST(c echo.Context) error {
|
||||
ctx := context.Background()
|
||||
targetId := slug.Make(c.FormValue("name"))
|
||||
|
||||
create := CreateTarget{
|
||||
Name: c.FormValue("name"),
|
||||
Group: strings.ToLower(c.FormValue("group")),
|
||||
Visibility: c.FormValue("visibility"),
|
||||
Metadata: c.FormValue("metadata"),
|
||||
}
|
||||
err := validator.New(validator.WithRequiredStructEnabled()).Struct(create)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := &models.Target{
|
||||
Name: create.Name,
|
||||
Group: create.Group,
|
||||
Id: targetId,
|
||||
Visibility: models.TargetVisibility(create.Visibility),
|
||||
State: models.TargetStateActive,
|
||||
Metadata: create.Metadata,
|
||||
}
|
||||
|
||||
err = services.CreateTarget(
|
||||
ctx,
|
||||
h.db,
|
||||
target,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, "/settings/targets")
|
||||
}
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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{
|
67
internal/server/services/check_history.go
Normal file
67
internal/server/services/check_history.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.temporal.io/api/workflowservice/v1"
|
||||
"go.temporal.io/sdk/client"
|
||||
)
|
||||
|
||||
type CheckHistory struct {
|
||||
CheckId string
|
||||
Status string
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
func GetLastNCheckHistory(ctx context.Context, temporal client.Client, n int32) ([]*CheckHistory, error) {
|
||||
var checkHistory []*CheckHistory
|
||||
|
||||
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
|
||||
PageSize: n,
|
||||
})
|
||||
if err != nil {
|
||||
return checkHistory, err
|
||||
}
|
||||
|
||||
executions := response.GetExecutions()
|
||||
|
||||
for _, execution := range executions {
|
||||
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
|
||||
checkId := scheduleId[len("check-"):]
|
||||
checkHistory = append(checkHistory, &CheckHistory{
|
||||
CheckId: checkId,
|
||||
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
|
||||
Status: execution.Status.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return checkHistory, nil
|
||||
}
|
||||
|
||||
func GetCheckHistoryForCheck(ctx context.Context, temporal client.Client, checkId string) ([]*CheckHistory, error) {
|
||||
var checkHistory []*CheckHistory
|
||||
|
||||
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
|
||||
PageSize: 10,
|
||||
Query: fmt.Sprintf(`TemporalScheduledById = "%s"`, getScheduleId(checkId)),
|
||||
})
|
||||
if err != nil {
|
||||
return checkHistory, err
|
||||
}
|
||||
|
||||
executions := response.GetExecutions()
|
||||
|
||||
for _, execution := range executions {
|
||||
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
|
||||
checkId := scheduleId[len("check-"):]
|
||||
checkHistory = append(checkHistory, &CheckHistory{
|
||||
CheckId: checkId,
|
||||
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
|
||||
Status: execution.Status.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return checkHistory, nil
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
75
internal/server/services/targets.go
Normal file
75
internal/server/services/targets.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mentos1386/zdravko/database/models"
|
||||
)
|
||||
|
||||
func CountTargets(ctx context.Context, db *sqlx.DB) (int, error) {
|
||||
var count int
|
||||
err := db.GetContext(ctx, &count, "SELECT COUNT(*) FROM targets")
|
||||
return count, err
|
||||
}
|
||||
|
||||
func SetTargetState(ctx context.Context, db *sqlx.DB, id string, state models.TargetState) error {
|
||||
_, err := db.NamedExecContext(ctx,
|
||||
`UPDATE targets SET state=:state WHERE id=:id`,
|
||||
struct {
|
||||
Id string
|
||||
State models.TargetState
|
||||
}{Id: id, State: state},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func CreateTarget(ctx context.Context, db *sqlx.DB, target *models.Target) error {
|
||||
_, err := db.NamedExecContext(ctx,
|
||||
`INSERT INTO targets (id, name, "group", visibility, state, metadata) VALUES (:id, :name, :group, :visibility, :state, :metadata)`,
|
||||
target,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateTarget(ctx context.Context, db *sqlx.DB, target *models.Target) error {
|
||||
_, err := db.NamedExecContext(ctx,
|
||||
`UPDATE targets SET visibility=:visibility, "group"=:group, metadata=:metadata WHERE id=:id`,
|
||||
target,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteTarget(ctx context.Context, db *sqlx.DB, id string) error {
|
||||
_, err := db.ExecContext(ctx,
|
||||
"DELETE FROM targets WHERE id=$1",
|
||||
id,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetTarget(ctx context.Context, db *sqlx.DB, id string) (*models.Target, error) {
|
||||
target := &models.Target{}
|
||||
err := db.GetContext(ctx, target,
|
||||
"SELECT * FROM targets WHERE id=$1",
|
||||
id,
|
||||
)
|
||||
return target, err
|
||||
}
|
||||
|
||||
func GetTargets(ctx context.Context, db *sqlx.DB) ([]*models.Target, error) {
|
||||
targets := []*models.Target{}
|
||||
err := db.SelectContext(ctx, &targets,
|
||||
"SELECT * FROM targets ORDER BY name",
|
||||
)
|
||||
return targets, err
|
||||
}
|
||||
|
||||
func GetTargetsWithFilter(ctx context.Context, db *sqlx.DB, filter string) ([]*models.Target, error) {
|
||||
targets := []*models.Target{}
|
||||
err := db.SelectContext(ctx, &targets,
|
||||
"SELECT * FROM targets WHERE name ILIKE $1 ORDER BY name",
|
||||
"%"+filter+"%",
|
||||
)
|
||||
return targets, err
|
||||
}
|
59
internal/server/services/targets_history.go
Normal file
59
internal/server/services/targets_history.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mentos1386/zdravko/database/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type TargetHistory struct {
|
||||
*models.TargetHistory
|
||||
TargetName string `db:"target_name"`
|
||||
}
|
||||
|
||||
func GetLastNTargetHistory(ctx context.Context, db *sqlx.DB, n int) ([]*TargetHistory, error) {
|
||||
var targetHistory []*TargetHistory
|
||||
err := db.SelectContext(ctx, &targetHistory, `
|
||||
SELECT
|
||||
th.*,
|
||||
t.name AS target_name
|
||||
FROM target_histories th
|
||||
LEFT JOIN targets t ON th.target_id = t.id
|
||||
WHERE th.target_id = $1
|
||||
ORDER BY th.created_at DESC
|
||||
LIMIT $1
|
||||
`, n)
|
||||
return targetHistory, err
|
||||
}
|
||||
|
||||
func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string) ([]*TargetHistory, error) {
|
||||
var targetHistory []*TargetHistory
|
||||
err := db.SelectContext(ctx, &targetHistory, `
|
||||
SELECT
|
||||
th.*,
|
||||
t.name AS target_name
|
||||
FROM target_histories th
|
||||
LEFT JOIN targets t ON th.target_id = t.id
|
||||
WHERE th.target_id = $1
|
||||
ORDER BY th.created_at DESC
|
||||
`, targetId)
|
||||
return targetHistory, err
|
||||
}
|
||||
|
||||
func AddHistoryForTarget(ctx context.Context, db *sqlx.DB, history *models.TargetHistory) error {
|
||||
_, err := db.NamedExecContext(ctx,
|
||||
`
|
||||
INSERT INTO target_histories (
|
||||
target_id,
|
||||
status,
|
||||
note
|
||||
) VALUES (
|
||||
:target_id,
|
||||
:status,
|
||||
:note
|
||||
)`,
|
||||
history,
|
||||
)
|
||||
return err
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
67
internal/server/services/trigger_history.go
Normal file
67
internal/server/services/trigger_history.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.temporal.io/api/workflowservice/v1"
|
||||
"go.temporal.io/sdk/client"
|
||||
)
|
||||
|
||||
type TriggerHistory struct {
|
||||
TriggerId string
|
||||
Status string
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
func GetLastNTriggerHistory(ctx context.Context, temporal client.Client, n int32) ([]*TriggerHistory, error) {
|
||||
var checkHistory []*TriggerHistory
|
||||
|
||||
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
|
||||
PageSize: n,
|
||||
})
|
||||
if err != nil {
|
||||
return checkHistory, err
|
||||
}
|
||||
|
||||
executions := response.GetExecutions()
|
||||
|
||||
for _, execution := range executions {
|
||||
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
|
||||
checkId := scheduleId[len("trigger-"):]
|
||||
checkHistory = append(checkHistory, &TriggerHistory{
|
||||
TriggerId: checkId,
|
||||
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
|
||||
Status: execution.Status.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return checkHistory, nil
|
||||
}
|
||||
|
||||
func GetTriggerHistoryForTrigger(ctx context.Context, temporal client.Client, checkId string) ([]*TriggerHistory, error) {
|
||||
var checkHistory []*TriggerHistory
|
||||
|
||||
response, err := temporal.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
|
||||
PageSize: 10,
|
||||
Query: fmt.Sprintf(`TemporalScheduledById = "%s"`, getScheduleId(checkId)),
|
||||
})
|
||||
if err != nil {
|
||||
return checkHistory, err
|
||||
}
|
||||
|
||||
executions := response.GetExecutions()
|
||||
|
||||
for _, execution := range executions {
|
||||
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
|
||||
checkId := scheduleId[len("check-"):]
|
||||
checkHistory = append(checkHistory, &TriggerHistory{
|
||||
TriggerId: checkId,
|
||||
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
|
||||
Status: execution.Status.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return checkHistory, nil
|
||||
}
|
|
@ -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"
|
58
internal/server/workflows/check.go
Normal file
58
internal/server/workflows/check.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package workflows
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/mentos1386/zdravko/internal/temporal"
|
||||
"github.com/mentos1386/zdravko/pkg/api"
|
||||
"go.temporal.io/sdk/workflow"
|
||||
)
|
||||
|
||||
func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param temporal.WorkflowCheckParam) (api.CheckStatus, error) {
|
||||
workerGroupIds := param.WorkerGroupIds
|
||||
sort.Strings(workerGroupIds)
|
||||
|
||||
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
StartToCloseTimeout: 60 * time.Second,
|
||||
TaskQueue: temporal.TEMPORAL_SERVER_QUEUE,
|
||||
})
|
||||
targetsFilterParam := temporal.ActivityTargetsFilterParam{
|
||||
Filter: param.Filter,
|
||||
}
|
||||
targetsFilterResult := temporal.ActivityTargetsFilterResult{}
|
||||
err := workflow.ExecuteActivity(ctx, temporal.ActivityTargetsFilterName, targetsFilterParam).Get(ctx, &targetsFilterResult)
|
||||
if err != nil {
|
||||
return api.CheckStatusUnknown, err
|
||||
}
|
||||
|
||||
for _, target := range targetsFilterResult.Targets {
|
||||
for _, workerGroupId := range workerGroupIds {
|
||||
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
||||
StartToCloseTimeout: 60 * time.Second,
|
||||
TaskQueue: workerGroupId,
|
||||
})
|
||||
|
||||
heatlcheckParam := temporal.ActivityCheckParam{
|
||||
Script: param.Script,
|
||||
Target: target,
|
||||
}
|
||||
|
||||
var checkResult *temporal.ActivityCheckResult
|
||||
err := workflow.ExecuteActivity(ctx, temporal.ActivityCheckName, heatlcheckParam).Get(ctx, &checkResult)
|
||||
if err != nil {
|
||||
return api.CheckStatusUnknown, err
|
||||
}
|
||||
|
||||
status := api.CheckStatusFailure
|
||||
if checkResult.Success {
|
||||
status = api.CheckStatusSuccess
|
||||
}
|
||||
|
||||
slog.Info("Check %s status: %s", param.CheckId, status)
|
||||
}
|
||||
}
|
||||
|
||||
return api.CheckStatusSuccess, nil
|
||||
}
|
9
internal/server/workflows/workflows.go
Normal file
9
internal/server/workflows/workflows.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package workflows
|
||||
|
||||
|
||||
type Workflows struct {
|
||||
}
|
||||
|
||||
func NewWorkflows() *Workflows {
|
||||
return &Workflows{}
|
||||
}
|
|
@ -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
|
||||
}
|
15
internal/temporal/activity_check.go
Normal file
15
internal/temporal/activity_check.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package temporal
|
||||
|
||||
import "github.com/mentos1386/zdravko/database/models"
|
||||
|
||||
type ActivityCheckParam struct {
|
||||
Script string
|
||||
Target *models.Target
|
||||
}
|
||||
|
||||
type ActivityCheckResult struct {
|
||||
Success bool
|
||||
Note string
|
||||
}
|
||||
|
||||
const ActivityCheckName = "CHECK"
|
10
internal/temporal/activity_process_check_outcome.go
Normal file
10
internal/temporal/activity_process_check_outcome.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package temporal
|
||||
|
||||
type ActivityProcessCheckOutcomeParam struct {
|
||||
Outcome string
|
||||
}
|
||||
|
||||
type ActivityProcessCheckOutcomeResult struct {
|
||||
}
|
||||
|
||||
const ActivityProcessCheckOutcomeName = "PROCESS_CHECK_OUTCOME"
|
15
internal/temporal/activity_targets_filter.go
Normal file
15
internal/temporal/activity_targets_filter.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package temporal
|
||||
|
||||
import (
|
||||
"github.com/mentos1386/zdravko/database/models"
|
||||
)
|
||||
|
||||
type ActivityTargetsFilterParam struct {
|
||||
Filter string
|
||||
}
|
||||
|
||||
type ActivityTargetsFilterResult struct {
|
||||
Targets []*models.Target
|
||||
}
|
||||
|
||||
const ActivityTargetsFilterName = "TARGETS_FILTER"
|
|
@ -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
|
||||
}
|
||||
|
|
10
internal/temporal/workflow_check.go
Normal file
10
internal/temporal/workflow_check.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package temporal
|
||||
|
||||
type WorkflowCheckParam struct {
|
||||
Script string
|
||||
Filter string
|
||||
CheckId string
|
||||
WorkerGroupIds []string
|
||||
}
|
||||
|
||||
const WorkflowCheckName = "CHECK_WORKFLOW"
|
16
internal/worker/activities/activities.go
Normal file
16
internal/worker/activities/activities.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package activities
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/mentos1386/zdravko/internal/config"
|
||||
)
|
||||
|
||||
type Activities struct {
|
||||
config *config.WorkerConfig
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewActivities(config *config.WorkerConfig, logger *slog.Logger) *Activities {
|
||||
return &Activities{config: config, logger: logger}
|
||||
}
|
21
internal/worker/activities/check.go
Normal file
21
internal/worker/activities/check.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package activities
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/mentos1386/zdravko/internal/temporal"
|
||||
"github.com/mentos1386/zdravko/pkg/k6"
|
||||
"github.com/mentos1386/zdravko/pkg/script"
|
||||
)
|
||||
|
||||
func (a *Activities) Check(ctx context.Context, param temporal.ActivityCheckParam) (*temporal.ActivityCheckResult, error) {
|
||||
execution := k6.NewExecution(slog.Default(), script.UnescapeString(param.Script))
|
||||
|
||||
result, err := execution.Run(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &temporal.ActivityCheckResult{Success: result.Success, Note: result.Note}, nil
|
||||
}
|
8
internal/worker/workflows/workflows.go
Normal file
8
internal/worker/workflows/workflows.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package workflows
|
||||
|
||||
type Workflows struct {
|
||||
}
|
||||
|
||||
func NewWorkflows() *Workflows {
|
||||
return &Workflows{}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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}
|
||||
}
|
13
justfile
13
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 {} \;
|
||||
|
||||
|
|
|
@ -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"`
|
||||
Status CheckStatus `json:"status"`
|
||||
Note string `json:"note"`
|
||||
WorkerGroupId string `json:"worker_group"`
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
|
@ -5,6 +5,8 @@ import (
|
|||
"log/slog"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mentos1386/zdravko/pkg/k6/zdravko"
|
||||
)
|
||||
|
||||
func getLogger() *slog.Logger {
|
||||
|
@ -18,12 +20,12 @@ func getLogger() *slog.Logger {
|
|||
}
|
||||
|
||||
func TestK6Success(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
logger := getLogger()
|
||||
|
||||
script := `
|
||||
import http from 'k6/http';
|
||||
import { sleep } from 'k6';
|
||||
import { getTarget } from 'k6/x/zdravko';
|
||||
|
||||
export const options = {
|
||||
vus: 10,
|
||||
|
@ -31,6 +33,8 @@ export const options = {
|
|||
};
|
||||
|
||||
export default function () {
|
||||
const target = getTarget();
|
||||
console.log('Target:', target);
|
||||
http.get('https://test.k6.io');
|
||||
sleep(1);
|
||||
}
|
||||
|
@ -38,6 +42,14 @@ export default function () {
|
|||
|
||||
execution := NewExecution(logger, script)
|
||||
|
||||
ctx := zdravko.WithZdravkoContext(context.Background(), zdravko.Context{Target: zdravko.Target{
|
||||
Name: "Test",
|
||||
Group: "Test",
|
||||
Metadata: map[string]interface{}{
|
||||
"Kind": "Test",
|
||||
},
|
||||
}})
|
||||
|
||||
result, err := execution.Run(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Error starting execution: %v", err)
|
||||
|
|
23
pkg/k6/zdravko/context.go
Normal file
23
pkg/k6/zdravko/context.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package zdravko
|
||||
|
||||
import "context"
|
||||
|
||||
type zdravkoContextKey string
|
||||
|
||||
type Target struct {
|
||||
Name string
|
||||
Group string
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
Target Target
|
||||
}
|
||||
|
||||
func WithZdravkoContext(ctx context.Context, zdravkoContext Context) context.Context {
|
||||
return context.WithValue(ctx, zdravkoContextKey("zdravko-ctx"), zdravkoContext)
|
||||
}
|
||||
|
||||
func GetZdravkoContext(ctx context.Context) Context {
|
||||
return ctx.Value(zdravkoContextKey("zdravko-ctx")).(Context)
|
||||
}
|
59
pkg/k6/zdravko/zdravko.go
Normal file
59
pkg/k6/zdravko/zdravko.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package zdravko
|
||||
|
||||
import (
|
||||
"go.k6.io/k6/js/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register("k6/x/zdravko", New())
|
||||
}
|
||||
|
||||
type (
|
||||
// RootModule is the global module instance that will create module
|
||||
// instances for each VU.
|
||||
RootModule struct{}
|
||||
|
||||
// ModuleInstance represents an instance of the JS module.
|
||||
ModuleInstance struct {
|
||||
// vu provides methods for accessing internal k6 objects for a VU
|
||||
vu modules.VU
|
||||
// comparator is the exported type
|
||||
zdravko *Zdravko
|
||||
}
|
||||
)
|
||||
|
||||
// Ensure the interfaces are implemented correctly.
|
||||
var (
|
||||
_ modules.Instance = &ModuleInstance{}
|
||||
_ modules.Module = &RootModule{}
|
||||
)
|
||||
|
||||
// New returns a pointer to a new RootModule instance.
|
||||
func New() *RootModule {
|
||||
return &RootModule{}
|
||||
}
|
||||
|
||||
// NewModuleInstance implements the modules.Module interface returning a new instance for each VU.
|
||||
func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
|
||||
return &ModuleInstance{
|
||||
vu: vu,
|
||||
zdravko: &Zdravko{vu: vu},
|
||||
}
|
||||
}
|
||||
|
||||
type Zdravko struct {
|
||||
vu modules.VU
|
||||
Targets []string
|
||||
}
|
||||
|
||||
func (z *Zdravko) GetTarget() Target {
|
||||
zdravkoContext := GetZdravkoContext(z.vu.Context())
|
||||
return zdravkoContext.Target
|
||||
}
|
||||
|
||||
// Exports implements the modules.Instance interface and returns the exported types for the JS module.
|
||||
func (mi *ModuleInstance) Exports() modules.Exports {
|
||||
return modules.Exports{
|
||||
Default: mi.zdravko,
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: "",
|
||||
}))
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -3,17 +3,17 @@ package worker
|
|||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/activities"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/temporal"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/workflows"
|
||||
"code.tjo.space/mentos1386/zdravko/pkg/api"
|
||||
"code.tjo.space/mentos1386/zdravko/pkg/retry"
|
||||
"github.com/mentos1386/zdravko/internal/config"
|
||||
"github.com/mentos1386/zdravko/internal/temporal"
|
||||
"github.com/mentos1386/zdravko/internal/worker/activities"
|
||||
"github.com/mentos1386/zdravko/pkg/api"
|
||||
"github.com/mentos1386/zdravko/pkg/retry"
|
||||
"github.com/pkg/errors"
|
||||
"go.temporal.io/sdk/activity"
|
||||
"go.temporal.io/sdk/worker"
|
||||
)
|
||||
|
||||
|
@ -60,11 +60,13 @@ func getConnectionConfig(token string, apiUrl string) (*ConnectionConfig, error)
|
|||
type Worker struct {
|
||||
worker worker.Worker
|
||||
cfg *config.WorkerConfig
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewWorker(cfg *config.WorkerConfig) (*Worker, error) {
|
||||
return &Worker{
|
||||
cfg: cfg,
|
||||
logger: slog.Default().WithGroup("worker"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -78,7 +80,7 @@ func (w *Worker) Start() error {
|
|||
return err
|
||||
}
|
||||
|
||||
log.Println("Worker Group:", config.Group)
|
||||
w.logger.Info("Worker Starting", "group", config.Group)
|
||||
|
||||
temporalClient, err := temporal.ConnectWorkerToTemporal(w.cfg.Token, config.Endpoint)
|
||||
if err != nil {
|
||||
|
@ -88,15 +90,10 @@ func (w *Worker) Start() error {
|
|||
// Create a new Worker
|
||||
w.worker = worker.New(temporalClient, config.Group, worker.Options{})
|
||||
|
||||
workerActivities := activities.NewActivities(w.cfg)
|
||||
workerWorkflows := workflows.NewWorkflows(workerActivities)
|
||||
|
||||
// Register Workflows
|
||||
w.worker.RegisterWorkflow(workerWorkflows.CheckWorkflowDefinition)
|
||||
workerActivities := activities.NewActivities(w.cfg, w.logger)
|
||||
|
||||
// Register Activities
|
||||
w.worker.RegisterActivity(workerActivities.Check)
|
||||
w.worker.RegisterActivity(workerActivities.CheckAddToHistory)
|
||||
w.worker.RegisterActivityWithOptions(workerActivities.Check, activity.RegisterOptions{Name: temporal.ActivityCheckName})
|
||||
|
||||
return w.worker.Run(worker.InterruptCh())
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
10
web/static/monaco/vs/basic-languages/yaml/yaml.js
Normal file
10
web/static/monaco/vs/basic-languages/yaml/yaml.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.46.0(21007360cad28648bdf46282a2592cb47c3a7a6f)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
define("vs/basic-languages/yaml/yaml", ["require","require"],(require)=>{
|
||||
"use strict";var moduleExports=(()=>{var m=Object.create;var l=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,f=Object.prototype.hasOwnProperty;var w=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(n,t)=>(typeof require<"u"?require:n)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var S=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports),k=(e,n)=>{for(var t in n)l(e,t,{get:n[t],enumerable:!0})},a=(e,n,t,i)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of p(n))!f.call(e,r)&&r!==t&&l(e,r,{get:()=>n[r],enumerable:!(i=b(n,r))||i.enumerable});return e},c=(e,n,t)=>(a(e,n,"default"),t&&a(t,n,"default")),u=(e,n,t)=>(t=e!=null?m(g(e)):{},a(n||!e||!e.__esModule?l(t,"default",{value:e,enumerable:!0}):t,e)),y=e=>a(l({},"__esModule",{value:!0}),e);var d=S((C,s)=>{var h=u(w("vs/editor/editor.api"));s.exports=h});var $={};k($,{conf:()=>N,language:()=>x});var o={};c(o,u(d()));var N={comments:{lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{offSide:!0},onEnterRules:[{beforeText:/:\s*$/,action:{indentAction:o.languages.IndentAction.Indent}}]},x={tokenPostfix:".yaml",brackets:[{token:"delimiter.bracket",open:"{",close:"}"},{token:"delimiter.square",open:"[",close:"]"}],keywords:["true","True","TRUE","false","False","FALSE","null","Null","Null","~"],numberInteger:/(?:0|[+-]?[0-9]+)/,numberFloat:/(?:0|[+-]?[0-9]+)(?:\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/,numberOctal:/0o[0-7]+/,numberHex:/0x[0-9a-fA-F]+/,numberInfinity:/[+-]?\.(?:inf|Inf|INF)/,numberNaN:/\.(?:nan|Nan|NAN)/,numberDate:/\d{4}-\d\d-\d\d([Tt ]\d\d:\d\d:\d\d(\.\d+)?(( ?[+-]\d\d?(:\d\d)?)|Z)?)?/,escapes:/\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,tokenizer:{root:[{include:"@whitespace"},{include:"@comment"},[/%[^ ]+.*$/,"meta.directive"],[/---/,"operators.directivesEnd"],[/\.{3}/,"operators.documentEnd"],[/[-?:](?= )/,"operators"],{include:"@anchor"},{include:"@tagHandle"},{include:"@flowCollections"},{include:"@blockStyle"},[/@numberInteger(?![ \t]*\S+)/,"number"],[/@numberFloat(?![ \t]*\S+)/,"number.float"],[/@numberOctal(?![ \t]*\S+)/,"number.octal"],[/@numberHex(?![ \t]*\S+)/,"number.hex"],[/@numberInfinity(?![ \t]*\S+)/,"number.infinity"],[/@numberNaN(?![ \t]*\S+)/,"number.nan"],[/@numberDate(?![ \t]*\S+)/,"number.date"],[/(".*?"|'.*?'|[^#'"]*?)([ \t]*)(:)( |$)/,["type","white","operators","white"]],{include:"@flowScalars"},[/.+?(?=(\s+#|$))/,{cases:{"@keywords":"keyword","@default":"string"}}]],object:[{include:"@whitespace"},{include:"@comment"},[/\}/,"@brackets","@pop"],[/,/,"delimiter.comma"],[/:(?= )/,"operators"],[/(?:".*?"|'.*?'|[^,\{\[]+?)(?=: )/,"type"],{include:"@flowCollections"},{include:"@flowScalars"},{include:"@tagHandle"},{include:"@anchor"},{include:"@flowNumber"},[/[^\},]+/,{cases:{"@keywords":"keyword","@default":"string"}}]],array:[{include:"@whitespace"},{include:"@comment"},[/\]/,"@brackets","@pop"],[/,/,"delimiter.comma"],{include:"@flowCollections"},{include:"@flowScalars"},{include:"@tagHandle"},{include:"@anchor"},{include:"@flowNumber"},[/[^\],]+/,{cases:{"@keywords":"keyword","@default":"string"}}]],multiString:[[/^( +).+$/,"string","@multiStringContinued.$1"]],multiStringContinued:[[/^( *).+$/,{cases:{"$1==$S2":"string","@default":{token:"@rematch",next:"@popall"}}}]],whitespace:[[/[ \t\r\n]+/,"white"]],comment:[[/#.*$/,"comment"]],flowCollections:[[/\[/,"@brackets","@array"],[/\{/,"@brackets","@object"]],flowScalars:[[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/'[^']*'/,"string"],[/"/,"string","@doubleQuotedString"]],doubleQuotedString:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],blockStyle:[[/[>|][0-9]*[+-]?$/,"operators","@multiString"]],flowNumber:[[/@numberInteger(?=[ \t]*[,\]\}])/,"number"],[/@numberFloat(?=[ \t]*[,\]\}])/,"number.float"],[/@numberOctal(?=[ \t]*[,\]\}])/,"number.octal"],[/@numberHex(?=[ \t]*[,\]\}])/,"number.hex"],[/@numberInfinity(?=[ \t]*[,\]\}])/,"number.infinity"],[/@numberNaN(?=[ \t]*[,\]\}])/,"number.nan"],[/@numberDate(?=[ \t]*[,\]\}])/,"number.date"]],tagHandle:[[/\![^ ]*/,"tag"]],anchor:[[/[&*][^ ]+/,"namespace"]]}};return y($);})();
|
||||
return moduleExports;
|
||||
});
|
|
@ -1,26 +1,26 @@
|
|||
{{ define "main" }}
|
||||
<div class="container max-w-screen-md flex flex-col mt-20 gap-20">
|
||||
{{ $length := len .Checks }}
|
||||
{{ $length := len .Targets }}
|
||||
{{ if eq $length 0 }}
|
||||
<section>
|
||||
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
|
||||
<h1
|
||||
class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl"
|
||||
>
|
||||
There are no checks yet.
|
||||
There are no targets yet.
|
||||
</h1>
|
||||
<p
|
||||
class="mb-8 text-l font-normal text-gray-700 lg:text-l sm:px-8 lg:px-40"
|
||||
>
|
||||
Create a check to check your services and get notified when they are
|
||||
down.
|
||||
Create a target to target your services and get notified when they
|
||||
are down.
|
||||
</p>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||
<a
|
||||
href="/settings/checks/create"
|
||||
href="/settings/targets/create"
|
||||
class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"
|
||||
>
|
||||
Create First Check
|
||||
Create First Target
|
||||
<svg class="feather ml-1 h-5 w-5 overflow-visible">
|
||||
<use href="/static/icons/feather-sprite.svg#plus" />
|
||||
</svg>
|
||||
|
@ -69,7 +69,7 @@
|
|||
</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="checks flex flex-col gap-4">
|
||||
<div class="targets flex flex-col gap-4">
|
||||
<div
|
||||
class="inline-flex gap-1 justify-center md:justify-end time-range"
|
||||
role="group"
|
||||
|
@ -93,7 +93,7 @@
|
|||
>90 Minutes</a
|
||||
>
|
||||
</div>
|
||||
{{ range $group, $checksAndStatus := .Checks }}
|
||||
{{ range $group, $targetsAndStatus := .Targets }}
|
||||
<details
|
||||
open
|
||||
class="bg-white shadow-md rounded-lg p-6 py-4 gap-2 [&_svg]:open:rotate-90"
|
||||
|
@ -101,11 +101,11 @@
|
|||
<summary
|
||||
class="flex flex-row gap-2 p-3 py-2 -mx-3 cursor-pointer hover:bg-blue-50 rounded-lg"
|
||||
>
|
||||
{{ if eq $checksAndStatus.Status "SUCCESS" }}
|
||||
{{ if eq $targetsAndStatus.Status "SUCCESS" }}
|
||||
<span
|
||||
class="flex w-3 h-3 bg-green-400 rounded-full self-center"
|
||||
></span>
|
||||
{{ else if eq $checksAndStatus.Status "FAILURE" }}
|
||||
{{ else if eq $targetsAndStatus.Status "FAILURE" }}
|
||||
<span
|
||||
class="flex w-3 h-3 bg-red-400 rounded-full self-center"
|
||||
></span>
|
||||
|
@ -123,7 +123,7 @@
|
|||
<use href="/static/icons/feather-sprite.svg#chevron-right" />
|
||||
</svg>
|
||||
</summary>
|
||||
{{ range $checksAndStatus.Checks }}
|
||||
{{ range $targetsAndStatus.Targets }}
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2 pb-2 border-b last-of-type:pb-0 last-of-type:border-0 border-gray-100"
|
||||
>
|
||||
|
|
|
@ -48,9 +48,8 @@
|
|||
</caption>
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col">Check Group</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Visibility</th>
|
||||
<th scope="col">Targets</th>
|
||||
<th scope="col">Worker Groups</th>
|
||||
<th scope="col">State</th>
|
||||
<th scope="col">Schedule</th>
|
||||
|
@ -58,47 +57,16 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .CheckGroups }}
|
||||
{{ $currentGroup := . }}
|
||||
<tr class="row-special">
|
||||
<th scope="rowgroup">
|
||||
{{ . }}
|
||||
</th>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{{ range $group, $checks := $.Checks }}
|
||||
{{ if eq $group $currentGroup }}
|
||||
{{ range $checks }}
|
||||
{{ range $checks := .Checks }}
|
||||
<tr>
|
||||
<th scope="row" aria-hidden="true">└─</th>
|
||||
<th scope="row">
|
||||
{{ .Name }}
|
||||
</th>
|
||||
<td>
|
||||
{{ if eq .Visibility "PUBLIC" }}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
|
||||
>
|
||||
Public
|
||||
</span>
|
||||
{{ else if eq .Visibility "PRIVATE" }}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-fuchsia-100 text-fuchsia-800"
|
||||
>
|
||||
Private
|
||||
</span>
|
||||
{{ else }}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
|
||||
>3</span
|
||||
>
|
||||
Unknown
|
||||
</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
{{ range .WorkerGroups }}
|
||||
|
@ -134,15 +102,10 @@
|
|||
{{ .Schedule }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/settings/checks/{{ .Id }}" class="link"
|
||||
>Details</a
|
||||
>
|
||||
<a href="/settings/checks/{{ .Id }}" class="link">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
|
|
@ -2,40 +2,20 @@
|
|||
<section class="p-5">
|
||||
<form action="/settings/checks/create" method="post">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" name="name" id="name" placeholder="Github.com" />
|
||||
<input type="text" name="name" id="name" placeholder="HTTP GET Request" />
|
||||
<p>Name of the check can be anything.</p>
|
||||
<label for="visibility">Visibility</label>
|
||||
<select name="visibility" id="visibility" required>
|
||||
<option value="PUBLIC">Public</option>
|
||||
<option value="PRIVATE">Private</option>
|
||||
</select>
|
||||
<p>
|
||||
Visibility determines who can see the check. If set to
|
||||
<code>public</code>, it will be visible to everyone on the homepage.
|
||||
Otherwise it will be only visible to signed in users.
|
||||
</p>
|
||||
<label for="group">Check Group</label>
|
||||
<input
|
||||
type="text"
|
||||
name="group"
|
||||
id="group"
|
||||
placeholder="default"
|
||||
value="default"
|
||||
required
|
||||
/>
|
||||
<p>
|
||||
Group checks together. This affects how they are presented on the
|
||||
homepage.
|
||||
</p>
|
||||
<label for="workergroups">Worker Groups</label>
|
||||
<input
|
||||
type="text"
|
||||
name="workergroups"
|
||||
id="workergroups"
|
||||
placeholder="NA EU"
|
||||
placeholder="europe asia"
|
||||
required
|
||||
/>
|
||||
<p>Worker groups are used to distribute the check to specific workers.</p>
|
||||
<p>
|
||||
Worker groups are used to distribute the check to specific workers.
|
||||
Space is a separator between groups.
|
||||
</p>
|
||||
<label for="schedule">Schedule</label>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -54,12 +34,30 @@
|
|||
<code>@daily</code>, <code>@weekly</code>, <code>@monthly</code>,
|
||||
<code>@yearly</code>.
|
||||
</p>
|
||||
<label for="script">Script</label>
|
||||
<textarea required id="script" name="script" class="h-96">
|
||||
{{ ScriptUnescapeString .Example }}</textarea
|
||||
<label for="filter">Filter</label>
|
||||
<textarea required id="filter" name="filter" class="h-12">
|
||||
{{ ScriptUnescapeString .ExampleFilter }}</textarea
|
||||
>
|
||||
<div
|
||||
id="editor"
|
||||
id="editor-filter"
|
||||
class="hidden block w-full h-12 rounded-lg border border-gray-300 overflow-hidden"
|
||||
></div>
|
||||
<p>
|
||||
With filter we specify what targets the check will run on. For whole
|
||||
grammar on what the filter query can look like, please read the
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/hashicorp/mql/blob/main/GRAMMAR.md"
|
||||
>MQL Grammar</a
|
||||
>
|
||||
documentation.
|
||||
</p>
|
||||
<label for="script">Script</label>
|
||||
<textarea required id="script" name="script" class="h-96">
|
||||
{{ ScriptUnescapeString .ExampleScript }}</textarea
|
||||
>
|
||||
<div
|
||||
id="editor-script"
|
||||
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
|
||||
></div>
|
||||
<p>
|
||||
|
@ -75,36 +73,42 @@
|
|||
|
||||
<script src="/static/monaco/vs/loader.js"></script>
|
||||
<script>
|
||||
function htmlDecode(input) {
|
||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||
return doc.documentElement.textContent;
|
||||
}
|
||||
script = htmlDecode("{{ .Example }}");
|
||||
|
||||
document.getElementById("editor").classList.remove("hidden");
|
||||
document.getElementById("script").hidden = true;
|
||||
const items = [
|
||||
{ name: "filter", language: "" },
|
||||
{ name: "script", language: "javascript" },
|
||||
];
|
||||
|
||||
function save() {
|
||||
const script = window.editor.getValue();
|
||||
document.getElementById("script").value = script;
|
||||
for (const { name } of items) {
|
||||
const elem = window.editors[name].getValue();
|
||||
document.getElementById(name).value = elem;
|
||||
}
|
||||
}
|
||||
|
||||
window.editors = {};
|
||||
for (const { name, language } of items) {
|
||||
const textarea = document.getElementById(name);
|
||||
const editor = document.getElementById("editor-" + name);
|
||||
|
||||
editor.classList.remove("hidden");
|
||||
textarea.hidden = true;
|
||||
|
||||
require.config({ paths: { vs: "/static/monaco/vs" } });
|
||||
require(["vs/editor/editor.main"], function () {
|
||||
window.editor = monaco.editor.create(document.getElementById("editor"), {
|
||||
value: script,
|
||||
language: "javascript",
|
||||
window.editors[name] = monaco.editor.create(editor, {
|
||||
value: textarea.value,
|
||||
language: language,
|
||||
minimap: { enabled: false },
|
||||
codeLens: false,
|
||||
contextmenu: false,
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
|
||||
const divElem = document.getElementById("editor");
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
window.editor.layout();
|
||||
window.editors[name].layout();
|
||||
});
|
||||
resizeObserver.observe(divElem);
|
||||
resizeObserver.observe(editor);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
|
|
@ -2,38 +2,6 @@
|
|||
<section class="p-5">
|
||||
<form action="/settings/checks/{{ .Check.Id }}" method="post">
|
||||
<h2>Configuration</h2>
|
||||
<label for="visibility">Visibility</label>
|
||||
<select name="visibility" id="visibility" required>
|
||||
<option
|
||||
{{ if eq .Check.Visibility "PUBLIC" }}selected="selected"{{ end }}
|
||||
value="PUBLIC"
|
||||
>
|
||||
Public
|
||||
</option>
|
||||
<option
|
||||
{{ if eq .Check.Visibility "PRIVATE" }}selected="selected"{{ end }}
|
||||
value="PRIVATE"
|
||||
>
|
||||
Private
|
||||
</option>
|
||||
</select>
|
||||
<p>
|
||||
Visibility determines who can see the check. If set to
|
||||
<code>public</code>, it will be visible to everyone on the homepage.
|
||||
Otherwise it will be only visible to signed in users.
|
||||
</p>
|
||||
<label for="group">Check Group</label>
|
||||
<input
|
||||
type="text"
|
||||
name="group"
|
||||
id="group"
|
||||
value="{{ .Check.Group }}"
|
||||
required
|
||||
/>
|
||||
<p>
|
||||
Group checks together. This affects how they are presented on the
|
||||
homepage.
|
||||
</p>
|
||||
<label for="workergroups">Worker Groups</label>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -60,12 +28,30 @@
|
|||
<code>@daily</code>, <code>@weekly</code>, <code>@monthly</code>,
|
||||
<code>@yearly</code>.
|
||||
</p>
|
||||
<label for="filter">Filter</label>
|
||||
<textarea required id="filter" name="filter" class="h-12">
|
||||
{{ ScriptUnescapeString .Check.Filter }}</textarea
|
||||
>
|
||||
<div
|
||||
id="editor-filter"
|
||||
class="hidden block w-full h-12 rounded-lg border border-gray-300 overflow-hidden"
|
||||
></div>
|
||||
<p>
|
||||
With filter we specify what targets the check will run on. For whole
|
||||
grammar on what the filter query can look like, please read the
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/hashicorp/mql/blob/main/GRAMMAR.md"
|
||||
>MQL Grammar</a
|
||||
>
|
||||
documentation.
|
||||
</p>
|
||||
<label for="script">Script</label>
|
||||
<textarea required id="script" name="script" class="h-96">
|
||||
{{ ScriptUnescapeString .Check.Script }}</textarea
|
||||
>
|
||||
<div
|
||||
id="editor"
|
||||
id="editor-script"
|
||||
class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden hidden"
|
||||
></div>
|
||||
<p>
|
||||
|
@ -135,6 +121,7 @@
|
|||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Check ID</th>
|
||||
<th>Status</th>
|
||||
<th>Worker Group</th>
|
||||
<th>Created At</th>
|
||||
|
@ -144,7 +131,9 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{{ range .History }}
|
||||
{{ if eq .Status "Running" }}
|
||||
<tr>
|
||||
<td>{{ .CheckId }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }}
|
||||
|
@ -160,54 +149,82 @@
|
|||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
|
||||
>
|
||||
{{ .WorkerGroupName }}
|
||||
{ .WorkerGroupName }
|
||||
</span>
|
||||
</td>
|
||||
<td>{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }</td>
|
||||
<td>/</td>
|
||||
<td class="whitespace-normal">/</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td>{{ .CheckId }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }}
|
||||
bg-green-100 text-green-800
|
||||
{{ else }}
|
||||
bg-red-100 text-red-800
|
||||
{{ end }}"
|
||||
>
|
||||
{{ .Status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
|
||||
</td>
|
||||
<td>{ .Duration }</td>
|
||||
<td class="whitespace-normal">
|
||||
{{ .Note }}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
|
||||
>
|
||||
{ .WorkerGroupName }
|
||||
</span>
|
||||
</td>
|
||||
<td>{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }</td>
|
||||
<td>{{ .Duration }}</td>
|
||||
<td class="whitespace-normal">{ .Note }</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<script src="/static/monaco/vs/loader.js"></script>
|
||||
<script>
|
||||
document.getElementById("editor").classList.remove("hidden");
|
||||
document.getElementById("script").hidden = true;
|
||||
<script>
|
||||
const items = [
|
||||
{ name: "filter", language: "" },
|
||||
{ name: "script", language: "javascript" },
|
||||
];
|
||||
|
||||
function save() {
|
||||
const script = window.editor.getValue();
|
||||
document.getElementById('script').value = script;
|
||||
for (const { name } of items) {
|
||||
const elem = window.editors[name].getValue();
|
||||
document.getElementById(name).value = elem;
|
||||
}
|
||||
}
|
||||
|
||||
function htmlDecode(input) {
|
||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||
return doc.documentElement.textContent;
|
||||
}
|
||||
script = htmlDecode("{{ .Check.Script }}")
|
||||
window.editors = {};
|
||||
for (const { name, language } of items) {
|
||||
const textarea = document.getElementById(name);
|
||||
const editor = document.getElementById("editor-" + name);
|
||||
|
||||
require.config({ paths: { vs: '/static/monaco/vs' } });
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
window.editor = monaco.editor.create(document.getElementById('editor'), {
|
||||
value: script,
|
||||
language: 'javascript',
|
||||
editor.classList.remove("hidden");
|
||||
textarea.hidden = true;
|
||||
|
||||
require.config({ paths: { vs: "/static/monaco/vs" } });
|
||||
require(["vs/editor/editor.main"], function () {
|
||||
window.editors[name] = monaco.editor.create(editor, {
|
||||
value: textarea.value,
|
||||
language: language,
|
||||
minimap: { enabled: false },
|
||||
codeLens: false,
|
||||
contextmenu: false,
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
|
||||
const divElem = document.getElementById('editor');
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
window.editor.layout();
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
window.editors[name].layout();
|
||||
});
|
||||
resizeObserver.observe(divElem);
|
||||
resizeObserver.observe(editor);
|
||||
});
|
||||
</script>
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
|
|
@ -21,9 +21,7 @@
|
|||
<div
|
||||
class="inline-block bg-white rounded-lg shadow p-5 text-center sm:text-left"
|
||||
>
|
||||
<h3 class="text-sm leading-6 font-medium text-gray-400">
|
||||
Total Checks
|
||||
</h3>
|
||||
<h3 class="text-sm leading-6 font-medium text-gray-400">Total Checks</h3>
|
||||
<p class="text-3xl font-bold text-black">{{ .ChecksCount }}</p>
|
||||
</div>
|
||||
<div
|
||||
|
@ -66,14 +64,14 @@
|
|||
<a
|
||||
class="underline hover:text-blue-600"
|
||||
href="/settings/checks/{{ .CheckId }}"
|
||||
>{{ .CheckName }}</a
|
||||
>{{ .CheckId }}</a
|
||||
>
|
||||
</th>
|
||||
<td>
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
|
||||
>
|
||||
{{ .WorkerGroupName }}
|
||||
{ .WorkerGroupName }
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -87,12 +85,8 @@
|
|||
{{ .Status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
|
||||
</td>
|
||||
<td class="whitespace-normal">
|
||||
{{ .Note }}
|
||||
</td>
|
||||
<td>{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }</td>
|
||||
<td class="whitespace-normal">{ .Note }</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
|
|
|
@ -55,32 +55,88 @@
|
|||
</a>
|
||||
</div>
|
||||
</caption>
|
||||
<thead>
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Action</th>
|
||||
<th scope="col">Target Group</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Visibility</th>
|
||||
<th scope="col">State</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{ range .Targets }}
|
||||
<tbody>
|
||||
{{ range .TargetGroups }}
|
||||
{{ $currentGroup := . }}
|
||||
<tr class="row-special">
|
||||
<th scope="rowgroup">
|
||||
{{ . }}
|
||||
</th>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{{ range $group, $targets := $.Targets }}
|
||||
{{ if eq $group $currentGroup }}
|
||||
{{ range $targets }}
|
||||
<tr>
|
||||
<th scope="row" aria-hidden="true">└─</th>
|
||||
<th scope="row">
|
||||
{{ .Name }}
|
||||
</th>
|
||||
<td>
|
||||
{{ if eq .Visibility "PUBLIC" }}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
|
||||
>
|
||||
{{ .Type }}
|
||||
Public
|
||||
</span>
|
||||
{{ else if eq .Visibility "PRIVATE" }}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-fuchsia-100 text-fuchsia-800"
|
||||
>
|
||||
Private
|
||||
</span>
|
||||
{{ else }}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
|
||||
>
|
||||
Unknown
|
||||
</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/settings/targets/{{ .Id }}" class="link">Details</a>
|
||||
{{ if eq .State "ACTIVE" }}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
|
||||
>
|
||||
ACTIVE
|
||||
</span>
|
||||
{{ else if eq .State "PAUSED" }}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
|
||||
>
|
||||
PAUSED
|
||||
</span>
|
||||
{{ else }}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
|
||||
>
|
||||
UNKNOWN
|
||||
</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/settings/targets/{{ .Id }}" class="link"
|
||||
>Details</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{{ end }}
|
||||
|
|
86
web/templates/pages/settings_targets_create.tmpl
Normal file
86
web/templates/pages/settings_targets_create.tmpl
Normal file
|
@ -0,0 +1,86 @@
|
|||
{{ define "settings" }}
|
||||
<section class="p-5">
|
||||
<form action="/settings/targets/create" method="post">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" name="name" id="name" placeholder="Github.com" />
|
||||
<p>Name of the target can be anything.</p>
|
||||
<label for="visibility">Visibility</label>
|
||||
<select name="visibility" id="visibility" required>
|
||||
<option value="PUBLIC">Public</option>
|
||||
<option value="PRIVATE">Private</option>
|
||||
</select>
|
||||
<p>
|
||||
Visibility determines who can see the target. If set to
|
||||
<code>public</code>, it will be visible to everyone on the homepage.
|
||||
Otherwise it will be only visible to signed in users.
|
||||
</p>
|
||||
<label for="group">Target Group</label>
|
||||
<input
|
||||
type="text"
|
||||
name="group"
|
||||
id="group"
|
||||
placeholder="default"
|
||||
value="default"
|
||||
required
|
||||
/>
|
||||
<p>
|
||||
Group targets together. This affects how they are presented on the
|
||||
homepage.
|
||||
</p>
|
||||
<label for="metadata">Metadata</label>
|
||||
<textarea required id="metadata" name="metadata" class="h-96">
|
||||
{{ ScriptUnescapeString .Example }}</textarea
|
||||
>
|
||||
<div
|
||||
id="editor-metadata"
|
||||
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
|
||||
></div>
|
||||
<p>
|
||||
Metadata is a YAML object that contains the configuration for the
|
||||
target. This configuration can be then used for <code>Checks</code> to
|
||||
filter the targets to act on as well as by using
|
||||
<code>getTarget()</code>
|
||||
function to fetch target metadata.
|
||||
</p>
|
||||
<button type="submit" onclick="save()">Create</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script src="/static/monaco/vs/loader.js"></script>
|
||||
<script>
|
||||
const items = [{ name: "metadata", language: "yaml" }];
|
||||
|
||||
function save() {
|
||||
for (const { name } of items) {
|
||||
const elem = window.editors[name].getValue();
|
||||
document.getElementById(name).value = elem;
|
||||
}
|
||||
}
|
||||
|
||||
window.editors = {};
|
||||
for (const { name, language } of items) {
|
||||
const textarea = document.getElementById(name);
|
||||
const editor = document.getElementById("editor-" + name);
|
||||
|
||||
editor.classList.remove("hidden");
|
||||
textarea.hidden = true;
|
||||
|
||||
require.config({ paths: { vs: "/static/monaco/vs" } });
|
||||
require(["vs/editor/editor.main"], function () {
|
||||
window.editors[name] = monaco.editor.create(editor, {
|
||||
value: textarea.value,
|
||||
language: language,
|
||||
minimap: { enabled: false },
|
||||
codeLens: false,
|
||||
contextmenu: false,
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
window.editors[name].layout();
|
||||
});
|
||||
resizeObserver.observe(editor);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
190
web/templates/pages/settings_targets_describe.tmpl
Normal file
190
web/templates/pages/settings_targets_describe.tmpl
Normal file
|
@ -0,0 +1,190 @@
|
|||
{{ define "settings" }}
|
||||
<section class="p-5">
|
||||
<form action="/settings/targets/{{ .Target.Id }}" method="post">
|
||||
<h2>Configuration</h2>
|
||||
<label for="visibility">Visibility</label>
|
||||
<select name="visibility" id="visibility" required>
|
||||
<option
|
||||
{{ if eq .Target.Visibility "PUBLIC" }}selected="selected"{{ end }}
|
||||
value="PUBLIC"
|
||||
>
|
||||
Public
|
||||
</option>
|
||||
<option
|
||||
{{ if eq .Target.Visibility "PRIVATE" }}selected="selected"{{ end }}
|
||||
value="PRIVATE"
|
||||
>
|
||||
Private
|
||||
</option>
|
||||
</select>
|
||||
<p>
|
||||
Visibility determines who can see the target. If set to
|
||||
<code>public</code>, it will be visible to everyone on the homepage.
|
||||
Otherwise it will be only visible to signed in users.
|
||||
</p>
|
||||
<label for="group">Target Group</label>
|
||||
<input
|
||||
type="text"
|
||||
name="group"
|
||||
id="group"
|
||||
value="{{ .Target.Group }}"
|
||||
required
|
||||
/>
|
||||
<p>
|
||||
Group targets together. This affects how they are presented on the
|
||||
homepage.
|
||||
</p>
|
||||
<label for="metadata">Metadata</label>
|
||||
<textarea required id="metadata" name="metadata" class="h-96">
|
||||
{{ ScriptUnescapeString .Target.Metadata }}</textarea
|
||||
>
|
||||
<div
|
||||
id="editor-metadata"
|
||||
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
|
||||
></div>
|
||||
<p>
|
||||
Metadata is a YAML object that contains the configuration for the
|
||||
target. This configuration can be then used for <code>Targets</code> to
|
||||
filter the targets to act on as well as by using
|
||||
<code>getTarget()</code>
|
||||
function to fetch target metadata.
|
||||
</p>
|
||||
<button type="submit" onclick="save()">Save</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="flex md:flex-row flex-col gap-4 h-min">
|
||||
<section class="p-5 flex-1">
|
||||
<h2 class="mb-2 flex flex-row gap-2">
|
||||
State
|
||||
{{ if eq .Target.State "ACTIVE" }}
|
||||
<span
|
||||
class="self-center h-fit w-fit px-2 text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
|
||||
>
|
||||
ACTIVE
|
||||
</span>
|
||||
{{ else if eq .Target.State "PAUSED" }}
|
||||
<span
|
||||
class="self-center h-fit w-fit px-2 text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
|
||||
>
|
||||
PAUSED
|
||||
</span>
|
||||
{{ end }}
|
||||
</h2>
|
||||
<p class="text-sm mb-2">
|
||||
Pausing the target will stop it from executing. This can be useful in
|
||||
cases of expected downtime. Or when the target is not needed anymore.
|
||||
</p>
|
||||
{{ if eq .Target.State "ACTIVE" }}
|
||||
<a
|
||||
class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100"
|
||||
href="/settings/targets/{{ .Target.Id }}/disable"
|
||||
>Pause</a
|
||||
>
|
||||
{{ else if eq .Target.State "PAUSED" }}
|
||||
<a
|
||||
class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100"
|
||||
href="/settings/targets/{{ .Target.Id }}/enable"
|
||||
>Resume</a
|
||||
>
|
||||
{{ end }}
|
||||
</section>
|
||||
|
||||
<section class="p-2 flex-1 border-4 border-red-300">
|
||||
<h2 class="mb-2">Danger Zone</h2>
|
||||
<p class="text-sm mb-2">Permanently delete this target.</p>
|
||||
<a
|
||||
class="block text-center focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2"
|
||||
href="/settings/targets/{{ .Target.Id }}/delete"
|
||||
>Delete</a
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<table>
|
||||
<caption>
|
||||
History
|
||||
<p>Last 10 executions of the targets.</p>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Worker Group</th>
|
||||
<th>Created At</th>
|
||||
<th>Duration</th>
|
||||
<th>Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .History }}
|
||||
<tr>
|
||||
<td>
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ if eq .Status "SUCCESS" }}
|
||||
bg-green-100 text-green-800
|
||||
{{ else }}
|
||||
bg-red-100 text-red-800
|
||||
{{ end }}"
|
||||
>
|
||||
{{ .Status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
|
||||
>
|
||||
{{ .WorkerGroupName }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
|
||||
</td>
|
||||
<td>{ .Duration }</td>
|
||||
<td class="whitespace-normal">
|
||||
{{ .Note }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<script src="/static/monaco/vs/loader.js"></script>
|
||||
<script>
|
||||
const items = [{ name: "metadata", language: "yaml" }];
|
||||
|
||||
function save() {
|
||||
for (const { name } of items) {
|
||||
const elem = window.editors[name].getValue();
|
||||
document.getElementById(name).value = elem;
|
||||
}
|
||||
}
|
||||
|
||||
window.editors = {};
|
||||
for (const { name, language } of items) {
|
||||
const textarea = document.getElementById(name);
|
||||
const editor = document.getElementById("editor-" + name);
|
||||
|
||||
editor.classList.remove("hidden");
|
||||
textarea.hidden = true;
|
||||
|
||||
require.config({ paths: { vs: "/static/monaco/vs" } });
|
||||
require(["vs/editor/editor.main"], function () {
|
||||
window.editors[name] = monaco.editor.create(editor, {
|
||||
value: textarea.value,
|
||||
language: language,
|
||||
minimap: { enabled: false },
|
||||
codeLens: false,
|
||||
contextmenu: false,
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
window.editors[name].layout();
|
||||
});
|
||||
resizeObserver.observe(editor);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
|
@ -14,7 +14,7 @@
|
|||
{{ ScriptUnescapeString .Example }}</textarea
|
||||
>
|
||||
<div
|
||||
id="editor"
|
||||
id="editor-script"
|
||||
class="hidden block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
|
||||
></div>
|
||||
<p>
|
||||
|
@ -30,36 +30,39 @@
|
|||
|
||||
<script src="/static/monaco/vs/loader.js"></script>
|
||||
<script>
|
||||
function htmlDecode(input) {
|
||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||
return doc.documentElement.textContent;
|
||||
}
|
||||
script = htmlDecode("{{ .Example }}");
|
||||
|
||||
document.getElementById("editor").classList.remove("hidden");
|
||||
document.getElementById("script").hidden = true;
|
||||
const items = [{ name: "script", language: "javascript" }];
|
||||
|
||||
function save() {
|
||||
const script = window.editor.getValue();
|
||||
document.getElementById("script").value = script;
|
||||
for (const { name } of items) {
|
||||
const elem = window.editors[name].getValue();
|
||||
document.getElementById(name).value = elem;
|
||||
}
|
||||
}
|
||||
|
||||
window.editors = {};
|
||||
for (const { name, language } of items) {
|
||||
const textarea = document.getElementById(name);
|
||||
const editor = document.getElementById("editor-" + name);
|
||||
|
||||
editor.classList.remove("hidden");
|
||||
textarea.hidden = true;
|
||||
|
||||
require.config({ paths: { vs: "/static/monaco/vs" } });
|
||||
require(["vs/editor/editor.main"], function () {
|
||||
window.editor = monaco.editor.create(document.getElementById("editor"), {
|
||||
value: script,
|
||||
language: "javascript",
|
||||
window.editors[name] = monaco.editor.create(editor, {
|
||||
value: textarea.value,
|
||||
language: language,
|
||||
minimap: { enabled: false },
|
||||
codeLens: false,
|
||||
contextmenu: false,
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
|
||||
const divElem = document.getElementById("editor");
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
window.editor.layout();
|
||||
window.editors[name].layout();
|
||||
});
|
||||
resizeObserver.observe(divElem);
|
||||
resizeObserver.observe(editor);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{{ ScriptUnescapeString .Trigger.Script }}</textarea
|
||||
>
|
||||
<div
|
||||
id="editor"
|
||||
id="editor-script"
|
||||
class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden hidden"
|
||||
></div>
|
||||
<p>
|
||||
|
@ -111,37 +111,40 @@
|
|||
</section>
|
||||
|
||||
<script src="/static/monaco/vs/loader.js"></script>
|
||||
<script>
|
||||
document.getElementById("editor").classList.remove("hidden");
|
||||
document.getElementById("script").hidden = true;
|
||||
<script>
|
||||
const items = [{ name: "script", language: "javascript" }];
|
||||
|
||||
function save() {
|
||||
const script = window.editor.getValue();
|
||||
document.getElementById('script').value = script;
|
||||
for (const { name } of items) {
|
||||
const elem = window.editors[name].getValue();
|
||||
document.getElementById(name).value = elem;
|
||||
}
|
||||
}
|
||||
|
||||
function htmlDecode(input) {
|
||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||
return doc.documentElement.textContent;
|
||||
}
|
||||
script = htmlDecode("{{ .Trigger.Script }}")
|
||||
window.editors = {};
|
||||
for (const { name, language } of items) {
|
||||
const textarea = document.getElementById(name);
|
||||
const editor = document.getElementById("editor-" + name);
|
||||
|
||||
require.config({ paths: { vs: '/static/monaco/vs' } });
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
window.editor = monaco.editor.create(document.getElementById('editor'), {
|
||||
value: script,
|
||||
language: 'javascript',
|
||||
editor.classList.remove("hidden");
|
||||
textarea.hidden = true;
|
||||
|
||||
require.config({ paths: { vs: "/static/monaco/vs" } });
|
||||
require(["vs/editor/editor.main"], function () {
|
||||
window.editors[name] = monaco.editor.create(editor, {
|
||||
value: textarea.value,
|
||||
language: language,
|
||||
minimap: { enabled: false },
|
||||
codeLens: false,
|
||||
contextmenu: false,
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
|
||||
const divElem = document.getElementById('editor');
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
window.editor.layout();
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
window.editors[name].layout();
|
||||
});
|
||||
resizeObserver.observe(divElem);
|
||||
resizeObserver.observe(editor);
|
||||
});
|
||||
</script>
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Reference in a new issue