diff --git a/internal/activities/monitor.go b/internal/activities/monitor.go index 4092752..74d38c3 100644 --- a/internal/activities/monitor.go +++ b/internal/activities/monitor.go @@ -9,6 +9,7 @@ import ( "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" ) @@ -23,7 +24,7 @@ type MonitorResult struct { } func (a *Activities) Monitor(ctx context.Context, param HealtcheckParam) (*MonitorResult, error) { - execution := k6.NewExecution(slog.Default(), param.Script) + execution := k6.NewExecution(slog.Default(), script.UnescapeString(param.Script)) result, err := execution.Run(ctx) if err != nil { diff --git a/internal/handlers/examples.yaml b/internal/handlers/examples.yaml new file mode 100644 index 0000000..10b3255 --- /dev/null +++ b/internal/handlers/examples.yaml @@ -0,0 +1,35 @@ +# Example trigger code +trigger: | + import kv from 'zdravko/kv'; + import slack from 'zdravko/notify/slack'; + + export default function (monitor, outcome) { + // If the outcome is not failure, we can reset the counter. + if (outcome.status !== 'FAILURE') { + return kv.delete(`${monitor.name}:issues:5min`); + } + + const count = kv.get(`${monitor.name}:issues:5min`) || 0; + + if (count > 5) { + slack.notify(`${monitor.name} has had more than 5 issues in the last 5 minutes`); + } + + // Increment and set TTL to 5 minutes + kv.increment(`${monitor.name}:issues:5min`, count + 1); + } + +# Example monitor code +monitor: | + import http from 'k6/http'; + + export const options = { + thresholds: { + // http errors should be less than 1% + http_req_failed: ['rate<0.01'], + }, + }; + + export default function () { + http.get('https://example.com'); + } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 76c8daf..bf2eb98 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -1,16 +1,27 @@ package handlers 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" "go.temporal.io/sdk/client" + "gopkg.in/yaml.v2" ) +//go:embed examples.yaml +var examplesYaml embed.FS + +type examples struct { + Monitor string `yaml:"monitor"` + Trigger string `yaml:"trigger"` +} + var Pages = []*components.Page{ {Path: "/", Title: "Status", Breadcrumb: "Status"}, {Path: "/incidents", Title: "Incidents", Breadcrumb: "Incidents"}, @@ -35,11 +46,26 @@ type BaseHandler struct { temporal client.Client store *sessions.CookieStore + + examples examples } func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Client, config *config.ServerConfig, logger *slog.Logger) *BaseHandler { store := sessions.NewCookieStore([]byte(config.SessionSecret)) + examples := examples{} + yamlFile, err := examplesYaml.ReadFile("examples.yaml") + if err != nil { + panic(err) + } + err = yaml.Unmarshal(yamlFile, &examples) + if err != nil { + panic(err) + } + + examples.Monitor = script.EscapeString(examples.Monitor) + examples.Trigger = script.EscapeString(examples.Trigger) + return &BaseHandler{ db: db, kvStore: kvStore, @@ -47,5 +73,6 @@ func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Clien logger: logger, temporal: temporal, store: store, + examples: examples, } } diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go index 16ab0c4..1d3185b 100644 --- a/internal/handlers/settings.go +++ b/internal/handlers/settings.go @@ -31,6 +31,8 @@ func NewSettings(user *AuthenticatedUser, page *components.Page, breadCrumbs []* var SettingsPages = []*components.Page{ {Path: "/settings", Title: "Overview", Breadcrumb: "Overview"}, + {Path: "/settings/targets", Title: "Targets", Breadcrumb: "Monitors"}, + {Path: "/settings/targets/create", Title: "Targets Create", Breadcrumb: "Create"}, {Path: "/settings/monitors", Title: "Monitors", Breadcrumb: "Monitors"}, {Path: "/settings/monitors/create", Title: "Monitors Create", Breadcrumb: "Create"}, {Path: "/settings/worker-groups", Title: "Worker Groups", Breadcrumb: "Worker Groups"}, @@ -45,6 +47,7 @@ var SettingsPages = []*components.Page{ var SettingsNavbar = []*components.Page{ GetPageByTitle(SettingsPages, "Overview"), + GetPageByTitle(SettingsPages, "Targets"), GetPageByTitle(SettingsPages, "Monitors"), GetPageByTitle(SettingsPages, "Triggers"), GetPageByTitle(SettingsPages, "Notifications"), diff --git a/internal/handlers/settings_targets.go b/internal/handlers/settings_targets.go new file mode 100644 index 0000000..072d849 --- /dev/null +++ b/internal/handlers/settings_targets.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "net/http" + + "code.tjo.space/mentos1386/zdravko/web/templates/components" + "github.com/labstack/echo/v4" +) + +type Target struct{} + +type SettingsTargets struct { + *Settings + Targets []*Target +} + +func (h *BaseHandler) SettingsTargetsGET(c echo.Context) error { + cc := c.(AuthenticatedContext) + + targets := make([]*Target, 0) + + return c.Render(http.StatusOK, "settings_targets.tmpl", &SettingsTargets{ + Settings: NewSettings( + cc.Principal.User, + GetPageByTitle(SettingsPages, "Targets"), + []*components.Page{GetPageByTitle(SettingsPages, "Targets")}, + ), + Targets: targets, + }) +} diff --git a/internal/handlers/settings_triggers.go b/internal/handlers/settings_triggers.go index b6fe172..491c268 100644 --- a/internal/handlers/settings_triggers.go +++ b/internal/handlers/settings_triggers.go @@ -6,6 +6,7 @@ import ( "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" @@ -20,28 +21,6 @@ type SettingsIncidents struct { Incidents []*Incident } -// FIXME: This should be moved in to a files to not worry about so much escaping. -const exampleTrigger = ` -import kv from 'zdravko/kv'; -import slack from 'zdravko/notify/slack'; - -export default function (monitor, outcome) { - // If the outcome is not failure, we can reset the counter. - if (outcome.status !== 'FAILURE') { - return kv.delete(` + "\\`\\${monitor.name}:issues:5min\\`" + `); - } - - const count = kv.get(` + "\\`\\${monitor.name}:issues:5min\\`" + `) || 0; - - if (count > 5) { - slack.notify(` + "\\`\\${monitor.name} has had more than 5 issues in the last 5 minutes\\`" + `); - } - - // Increment and set TTL to 5 minutes - kv.increment(` + "\\`\\${monitor.name}:issues:5min\\`" + `, count + 1); -} -` - type CreateTrigger struct { Name string `validate:"required"` Script string `validate:"required"` @@ -160,7 +139,7 @@ func (h *BaseHandler) SettingsTriggersDescribePOST(c echo.Context) error { triggerId := c.Param("id") update := UpdateTrigger{ - Script: c.FormValue("script"), + Script: script.EscapeString(c.FormValue("script")), } err := validator.New(validator.WithRequiredStructEnabled()).Struct(update) if err != nil { @@ -197,7 +176,7 @@ func (h *BaseHandler) SettingsTriggersCreateGET(c echo.Context) error { GetPageByTitle(SettingsPages, "Triggers Create"), }, ), - Example: exampleTrigger, + Example: h.examples.Trigger, }) } @@ -207,7 +186,7 @@ func (h *BaseHandler) SettingsTriggersCreatePOST(c echo.Context) error { create := CreateTrigger{ Name: c.FormValue("name"), - Script: c.FormValue("script"), + Script: script.EscapeString(c.FormValue("script")), } err := validator.New(validator.WithRequiredStructEnabled()).Struct(create) if err != nil { diff --git a/internal/handlers/settingsmonitors.go b/internal/handlers/settingsmonitors.go index ffcc06f..0c7b9ea 100644 --- a/internal/handlers/settingsmonitors.go +++ b/internal/handlers/settingsmonitors.go @@ -9,6 +9,7 @@ import ( "strings" "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" @@ -16,21 +17,6 @@ import ( "github.com/labstack/echo/v4" ) -const example = ` -import http from 'k6/http'; - -export const options = { - thresholds: { - // http errors should be less than 1% - http_req_failed: ['rate<0.01'], - }, -}; - -export default function () { - http.get('https://example.com'); -} -` - type CreateMonitor struct { Name string `validate:"required"` Group string `validate:"required"` @@ -211,7 +197,7 @@ func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error { Group: strings.ToLower(c.FormValue("group")), WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))), Schedule: c.FormValue("schedule"), - Script: c.FormValue("script"), + Script: script.EscapeString(c.FormValue("script")), } err := validator.New(validator.WithRequiredStructEnabled()).Struct(update) if err != nil { @@ -280,7 +266,7 @@ func (h *BaseHandler) SettingsMonitorsCreateGET(c echo.Context) error { GetPageByTitle(SettingsPages, "Monitors Create"), }, ), - Example: example, + Example: h.examples.Monitor, }) } @@ -293,7 +279,7 @@ func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error { Group: strings.ToLower(c.FormValue("group")), WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))), Schedule: c.FormValue("schedule"), - Script: c.FormValue("script"), + Script: script.EscapeString(c.FormValue("script")), } err := validator.New(validator.WithRequiredStructEnabled()).Struct(create) if err != nil { diff --git a/internal/script/script.go b/internal/script/script.go new file mode 100644 index 0000000..1e16249 --- /dev/null +++ b/internal/script/script.go @@ -0,0 +1,16 @@ +package script + +import ( + "html" + "regexp" + "strings" +) + +func EscapeString(s string) string { + re := regexp.MustCompile(`\r?\n`) + return re.ReplaceAllString(html.EscapeString(s), `\n`) +} + +func UnescapeString(s string) string { + return html.UnescapeString(strings.Replace(s, `\n`, "\n", -1)) +} diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 66fa22a..91b47ed 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -63,6 +63,15 @@ func Routes( settings.GET("/triggers/:id/disable", h.SettingsTriggersDisableGET) 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("/monitors", h.SettingsMonitorsGET) settings.GET("/monitors/create", h.SettingsMonitorsCreateGET) settings.POST("/monitors/create", h.SettingsMonitorsCreatePOST) diff --git a/web/static/css/main.css b/web/static/css/main.css index 885cb6b..8714499 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -79,7 +79,7 @@ code { @apply grid grid-cols-1 gap-5 grid-rows-[min-content] h-fit; } .settings section { - @apply relative overflow-x-auto shadow-md sm:rounded-lg text-gray-500 bg-white h-min; + @apply relative overflow-x-auto shadow-md sm:rounded-lg text-gray-700 bg-white h-min; } .settings section h2 { @@ -110,7 +110,7 @@ code { @apply p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white; } .settings section table caption p { - @apply mt-1 text-sm font-normal text-gray-500; + @apply mt-1 text-sm font-normal text-gray-700; } .settings section table thead { @apply text-xs text-gray-700 uppercase bg-gray-50; diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index 4a9cb8b..e455d91 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -833,10 +833,6 @@ video { justify-content: center; } -.justify-items-center { - justify-items: center; -} - .gap-1 { gap: 0.25rem; } @@ -861,12 +857,6 @@ video { gap: 1px; } -.space-y-4 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(1rem * var(--tw-space-y-reverse)); -} - .self-center { align-self: center; } @@ -949,6 +939,11 @@ video { background-color: rgb(29 78 216 / var(--tw-bg-opacity)); } +.bg-fuchsia-100 { + --tw-bg-opacity: 1; + background-color: rgb(250 232 255 / var(--tw-bg-opacity)); +} + .bg-gray-100 { --tw-bg-opacity: 1; background-color: rgb(243 244 246 / var(--tw-bg-opacity)); @@ -1173,6 +1168,11 @@ video { color: rgb(30 64 175 / var(--tw-text-opacity)); } +.text-fuchsia-800 { + --tw-text-opacity: 1; + color: rgb(134 25 143 / var(--tw-text-opacity)); +} + .text-gray-400 { --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); @@ -1472,7 +1472,7 @@ code { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); --tw-text-opacity: 1; - color: rgb(107 114 128 / var(--tw-text-opacity)); + color: rgb(55 65 81 / var(--tw-text-opacity)); --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); @@ -1624,7 +1624,7 @@ code { line-height: 1.25rem; font-weight: 400; --tw-text-opacity: 1; - color: rgb(107 114 128 / var(--tw-text-opacity)); + color: rgb(55 65 81 / var(--tw-text-opacity)); } .settings section table thead { @@ -1693,10 +1693,6 @@ code { letter-spacing: 0.05em; } -.\*\:w-full > * { - width: 100%; -} - .last-of-type\:border-0:last-of-type { border-width: 0px; } @@ -1801,12 +1797,6 @@ code { justify-content: center; } - .sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(0px * var(--tw-space-y-reverse)); - } - .sm\:px-8 { padding-left: 2rem; padding-right: 2rem; diff --git a/web/templates/components/base.tmpl b/web/templates/components/base.tmpl index 217fc2c..a6838df 100644 --- a/web/templates/components/base.tmpl +++ b/web/templates/components/base.tmpl @@ -16,7 +16,7 @@