mirror of
https://github.com/mentos1386/zdravko.git
synced 2024-11-21 23:33:34 +00:00
feat(workers): initial worker authentication and provisioning
This commit is contained in:
parent
2b6e1ca09b
commit
478232bcda
29 changed files with 916 additions and 103 deletions
8
.github/workflows/deploy.yaml
vendored
Normal file
8
.github/workflows/deploy.yaml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
name: Deploy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -5,5 +5,8 @@ zdravko.db
|
|||
temporal.db
|
||||
temporal.db-journal
|
||||
|
||||
# Keys
|
||||
*.pem
|
||||
|
||||
# Config
|
||||
.env
|
||||
|
|
|
@ -29,6 +29,9 @@ Demo is available at https://zdravko.fly.dev.
|
|||
# Configure
|
||||
cp example.env .env
|
||||
|
||||
# Generate JWT key
|
||||
just generate-jwt-key
|
||||
|
||||
# Start development environment
|
||||
just run
|
||||
```
|
||||
|
|
|
@ -14,9 +14,6 @@ COPY . ./
|
|||
# Build
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -o /bin/zdravko cmd/zdravko/main.go
|
||||
|
||||
# Prepare the data directory
|
||||
RUN mkdir -p /data
|
||||
|
||||
###
|
||||
# Final production
|
||||
FROM gcr.io/distroless/base-debian12:latest as production
|
||||
|
@ -34,7 +31,6 @@ EXPOSE 7233
|
|||
|
||||
# Volume to persist sqlite databases
|
||||
VOLUME /data
|
||||
COPY --from=builder --chown=nonroot:nonroot /data /data
|
||||
|
||||
ENV DATABASE_PATH=/data/zdravko.db
|
||||
ENV TEMPORAL_DATABASE_PATH=/data/temporal.db
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"code.tjo.space/mentos1386/zdravko/pkg/server"
|
||||
|
@ -85,7 +86,7 @@ func main() {
|
|||
}
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
log.Printf("Received signal: %v", sig)
|
||||
|
@ -97,7 +98,7 @@ func main() {
|
|||
println("Stopping", srv.Name())
|
||||
err := srv.Stop()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to stop server %s: %v", srv.Name(), err)
|
||||
log.Printf("Unable to stop server %s: %v", srv.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
9
go.mod
9
go.mod
|
@ -3,8 +3,13 @@ module code.tjo.space/mentos1386/zdravko
|
|||
go 1.21.6
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.18.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/gosimple/slug v1.13.1
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/temporalio/ui-server/v2 v2.23.0
|
||||
go.temporal.io/sdk v1.26.0-rc.2
|
||||
go.temporal.io/server v1.22.4
|
||||
|
@ -45,11 +50,9 @@ require (
|
|||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.18.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/gocql/gocql v1.5.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/mock v1.7.0-rc.1 // indirect
|
||||
|
@ -60,7 +63,6 @@ require (
|
|||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gosimple/slug v1.13.1 // indirect
|
||||
github.com/gosimple/unidecode v1.0.1 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
|
||||
|
@ -109,7 +111,6 @@ require (
|
|||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.18.2 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
|
|
8
go.sum
8
go.sum
|
@ -63,7 +63,6 @@ github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpA
|
|||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/crossdock/crossdock-go v0.0.0-20160816171116-049aabb0122b/go.mod h1:v9FBN7gdVTpiD/+LZ7Po0UKvROyT87uLVxTHVky/dlQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -87,6 +86,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
|
|||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
|
@ -105,6 +106,8 @@ github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
|||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
|
@ -126,6 +129,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
|
|||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
|
@ -302,7 +307,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/spf13/viper"
|
||||
|
@ -16,12 +15,17 @@ type Config struct {
|
|||
DatabasePath string `validate:"required"`
|
||||
SessionSecret string `validate:"required"`
|
||||
|
||||
Jwt Jwt `validate:"required"`
|
||||
OAuth2 OAuth2 `validate:"required"`
|
||||
|
||||
Temporal Temporal `validate:"required"`
|
||||
|
||||
HealthChecks []Healthcheck
|
||||
CronJobs []CronJob
|
||||
Worker Worker `validate:"required"`
|
||||
}
|
||||
|
||||
type Jwt struct {
|
||||
PrivateKey string `validate:"required"`
|
||||
PublicKey string `validate:"required"`
|
||||
}
|
||||
|
||||
type OAuth2 struct {
|
||||
|
@ -41,30 +45,8 @@ type Temporal struct {
|
|||
ServerHost string `validate:"required"`
|
||||
}
|
||||
|
||||
type HealthCheckHTTP struct {
|
||||
URL string `validate:"required,url"`
|
||||
Method string `validate:"required,oneof=GET POST PUT"`
|
||||
}
|
||||
|
||||
type HealthCheckTCP struct {
|
||||
Host string `validate:"required,hostname"`
|
||||
Port int `validate:"required,gte=1,lte=65535"`
|
||||
}
|
||||
|
||||
type Healthcheck struct {
|
||||
Name string `validate:"required"`
|
||||
Retries int `validate:"optional,gte=0"`
|
||||
Schedule string `validate:"required,cron"`
|
||||
Timeout time.Duration `validate:"required"`
|
||||
|
||||
HTTP HealthCheckHTTP `validate:"required"`
|
||||
TCP HealthCheckTCP `validate:"required"`
|
||||
}
|
||||
|
||||
type CronJob struct {
|
||||
Name string `validate:"required"`
|
||||
Schedule string `validate:"required,cron"`
|
||||
Buffer time.Duration `validate:"required"`
|
||||
type Worker struct {
|
||||
Token string `validate:"required"`
|
||||
}
|
||||
|
||||
func GetEnvOrDefault(key, def string) string {
|
||||
|
@ -93,6 +75,8 @@ func NewConfig() *Config {
|
|||
viper.SetDefault("temporal.listenaddress", GetEnvOrDefault("TEMPORAL_LISTEN_ADDRESS", "0.0.0.0"))
|
||||
viper.SetDefault("temporal.uihost", GetEnvOrDefault("TEMPORAL_UI_HOST", "127.0.0.1:8223"))
|
||||
viper.SetDefault("temporal.serverhost", GetEnvOrDefault("TEMPORAL_SERVER_HOST", "127.0.0.1:7233"))
|
||||
viper.SetDefault("jwt.privatekey", os.Getenv("JWT_PRIVATE_KEY"))
|
||||
viper.SetDefault("jwt.publickey", os.Getenv("JWT_PUBLIC_KEY"))
|
||||
viper.SetDefault("oauth2.clientid", os.Getenv("OAUTH2_CLIENT_ID"))
|
||||
viper.SetDefault("oauth2.clientsecret", os.Getenv("OAUTH2_CLIENT_SECRET"))
|
||||
viper.SetDefault("oauth2.scopes", GetEnvOrDefault("OAUTH2_ENDPOINT_SCOPES", "openid profile email"))
|
||||
|
@ -100,6 +84,7 @@ func NewConfig() *Config {
|
|||
viper.SetDefault("oauth2.endpointauthurl", os.Getenv("OAUTH2_ENDPOINT_AUTH_URL"))
|
||||
viper.SetDefault("oauth2.endpointuserinfourl", os.Getenv("OAUTH2_ENDPOINT_USER_INFO_URL"))
|
||||
viper.SetDefault("oauth2.endpointlogouturl", GetEnvOrDefault("OAUTH2_ENDPOINT_LOGOUT_URL", ""))
|
||||
viper.SetDefault("worker.token", os.Getenv("WORKER_TOKEN"))
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
|
|
|
@ -15,6 +15,7 @@ func ConnectToDatabase(path string) (*gorm.DB, *query.Query, error) {
|
|||
}
|
||||
|
||||
err = db.AutoMigrate(
|
||||
models.Worker{},
|
||||
models.HealthcheckHttp{},
|
||||
models.HealthcheckHttpHistory{},
|
||||
models.HealthcheckTcp{},
|
||||
|
|
|
@ -35,6 +35,7 @@ var SettingsPages = []*components.Page{
|
|||
{Path: "/settings/healthchecks/create", Title: "Healthchecks Create", Breadcrumb: "Create"},
|
||||
{Path: "/settings/cronjobs", Title: "Cronjobs", Breadcrumb: "Cronjobs"},
|
||||
{Path: "/settings/workers", Title: "Workers", Breadcrumb: "Workers"},
|
||||
{Path: "/settings/workers/create", Title: "Workers Create", Breadcrumb: "Create"},
|
||||
{Path: "/temporal", Title: "Temporal", Breadcrumb: "Temporal"},
|
||||
{Path: "/oauth2/logout", Title: "Logout", Breadcrumb: "Logout"},
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"code.tjo.space/mentos1386/zdravko/internal/services"
|
||||
"code.tjo.space/mentos1386/zdravko/web/templates"
|
||||
"code.tjo.space/mentos1386/zdravko/web/templates/components"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gosimple/slug"
|
||||
)
|
||||
|
@ -130,7 +131,12 @@ func (h *BaseHandler) SettingsHealthchecksCreatePOST(w http.ResponseWriter, r *h
|
|||
Method: r.FormValue("method"),
|
||||
}
|
||||
|
||||
err := services.CreateHealthcheckHttp(
|
||||
err := validator.New(validator.WithRequiredStructEnabled()).Struct(healthcheckHttp)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
err = services.CreateHealthcheckHttp(
|
||||
ctx,
|
||||
h.db,
|
||||
healthcheckHttp,
|
||||
|
|
164
internal/handlers/settingsworkers.go
Normal file
164
internal/handlers/settingsworkers.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/jwt"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/models"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/services"
|
||||
"code.tjo.space/mentos1386/zdravko/web/templates"
|
||||
"code.tjo.space/mentos1386/zdravko/web/templates/components"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gosimple/slug"
|
||||
)
|
||||
|
||||
type SettingsWorkers struct {
|
||||
*Settings
|
||||
Workers []*models.Worker
|
||||
WorkersLength int
|
||||
}
|
||||
|
||||
type SettingsWorker struct {
|
||||
*Settings
|
||||
Worker *models.Worker
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsWorkersGET(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) {
|
||||
ts, err := template.ParseFS(templates.Templates,
|
||||
"components/base.tmpl",
|
||||
"components/settings.tmpl",
|
||||
"pages/settings_workers.tmpl",
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
workers, err := h.query.Worker.WithContext(context.Background()).Find()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
err = ts.ExecuteTemplate(w, "base", &SettingsWorkers{
|
||||
Settings: NewSettings(
|
||||
user,
|
||||
GetPageByTitle(SettingsPages, "Workers"),
|
||||
[]*components.Page{GetPageByTitle(SettingsPages, "Workers")},
|
||||
),
|
||||
Workers: workers,
|
||||
WorkersLength: len(workers),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsWorkersDescribeGET(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) {
|
||||
vars := mux.Vars(r)
|
||||
slug := vars["slug"]
|
||||
|
||||
ts, err := template.ParseFS(templates.Templates,
|
||||
"components/base.tmpl",
|
||||
"components/settings.tmpl",
|
||||
"pages/settings_workers_describe.tmpl",
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
worker, err := services.GetWorker(context.Background(), h.query, slug)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
err = ts.ExecuteTemplate(w, "base", &SettingsWorker{
|
||||
Settings: NewSettings(
|
||||
user,
|
||||
GetPageByTitle(SettingsPages, "Workers"),
|
||||
[]*components.Page{
|
||||
GetPageByTitle(SettingsPages, "Workers"),
|
||||
{
|
||||
Path: fmt.Sprintf("/settings/workers/%s", slug),
|
||||
Title: "Describe",
|
||||
Breadcrumb: worker.Name,
|
||||
},
|
||||
}),
|
||||
Worker: worker,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsWorkersCreateGET(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) {
|
||||
ts, err := template.ParseFS(templates.Templates,
|
||||
"components/base.tmpl",
|
||||
"components/settings.tmpl",
|
||||
"pages/settings_workers_create.tmpl",
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = ts.ExecuteTemplate(w, "base", NewSettings(
|
||||
user,
|
||||
GetPageByTitle(SettingsPages, "Workers"),
|
||||
[]*components.Page{
|
||||
GetPageByTitle(SettingsPages, "Workers"),
|
||||
GetPageByTitle(SettingsPages, "Workers Create"),
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsWorkersCreatePOST(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) {
|
||||
ctx := context.Background()
|
||||
|
||||
worker := &models.Worker{
|
||||
Name: r.FormValue("name"),
|
||||
Slug: slug.Make(r.FormValue("name")),
|
||||
}
|
||||
|
||||
err := validator.New(validator.WithRequiredStructEnabled()).Struct(worker)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
err = services.CreateWorker(
|
||||
ctx,
|
||||
h.db,
|
||||
worker,
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/settings/workers", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SettingsWorkersTokenGET(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) {
|
||||
vars := mux.Vars(r)
|
||||
slug := vars["slug"]
|
||||
|
||||
worker, err := services.GetWorker(context.Background(), h.query, slug)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Allow write access to default namespace
|
||||
token, err := jwt.NewToken(h.config, []string{"default:write"}, worker.Slug)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"token": "` + token + `"}`))
|
||||
}
|
64
internal/jwt/jwt.go
Normal file
64
internal/jwt/jwt.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func JwtPublicKeyID(key *rsa.PublicKey) string {
|
||||
hash := sha256.Sum256(key.N.Bytes())
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func JwtPrivateKey(c *config.Config) (*rsa.PrivateKey, error) {
|
||||
return jwt.ParseRSAPrivateKeyFromPEM([]byte(c.Jwt.PrivateKey))
|
||||
}
|
||||
|
||||
func JwtPublicKey(c *config.Config) (*rsa.PublicKey, error) {
|
||||
return jwt.ParseRSAPublicKeyFromPEM([]byte(c.Jwt.PublicKey))
|
||||
}
|
||||
|
||||
// Ref: https://docs.temporal.io/self-hosted-guide/security#authorization
|
||||
func NewToken(config *config.Config, permissions []string, subject string) (string, error) {
|
||||
privateKey, err := JwtPrivateKey(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
publicKey, err := JwtPublicKey(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
type WorkerClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
// Create claims with multiple fields populated
|
||||
claims := WorkerClaims{
|
||||
jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(12 * 30 * 24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "zdravko",
|
||||
Subject: subject,
|
||||
},
|
||||
permissions,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
token.Header["kid"] = JwtPublicKeyID(publicKey)
|
||||
|
||||
signedToken, err := token.SignedString(privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
}
|
|
@ -11,27 +11,34 @@ type OAuth2State struct {
|
|||
Expiry time.Time
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"unique" validate:"required"`
|
||||
Slug string `gorm:"unique"`
|
||||
Status string
|
||||
}
|
||||
|
||||
type Healthcheck struct {
|
||||
gorm.Model
|
||||
Slug string `gorm:"unique"`
|
||||
Name string `gorm:"unique"`
|
||||
Name string `gorm:"unique" validate:"required"`
|
||||
Status string // UP, DOWN
|
||||
UptimePercentage float64
|
||||
Schedule string
|
||||
Schedule string `validate:"required,cron"`
|
||||
}
|
||||
|
||||
type HealthcheckHttp struct {
|
||||
gorm.Model
|
||||
Healthcheck
|
||||
Url string
|
||||
Method string
|
||||
Url string `validate:"required,url"`
|
||||
Method string `validate:"required,oneof=GET POST"`
|
||||
}
|
||||
|
||||
type HealthcheckTcp struct {
|
||||
gorm.Model
|
||||
Healthcheck
|
||||
Hostname string
|
||||
Port int
|
||||
Hostname string `validate:"required,hostname"`
|
||||
Port int `validate:"required,gte=1,lte=65535"`
|
||||
}
|
||||
|
||||
type Cronjob struct {
|
||||
|
|
|
@ -24,6 +24,7 @@ var (
|
|||
HealthcheckTcp *healthcheckTcp
|
||||
HealthcheckTcpHistory *healthcheckTcpHistory
|
||||
OAuth2State *oAuth2State
|
||||
Worker *worker
|
||||
)
|
||||
|
||||
func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
||||
|
@ -35,6 +36,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
|||
HealthcheckTcp = &Q.HealthcheckTcp
|
||||
HealthcheckTcpHistory = &Q.HealthcheckTcpHistory
|
||||
OAuth2State = &Q.OAuth2State
|
||||
Worker = &Q.Worker
|
||||
}
|
||||
|
||||
func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
||||
|
@ -47,6 +49,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
|||
HealthcheckTcp: newHealthcheckTcp(db, opts...),
|
||||
HealthcheckTcpHistory: newHealthcheckTcpHistory(db, opts...),
|
||||
OAuth2State: newOAuth2State(db, opts...),
|
||||
Worker: newWorker(db, opts...),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,6 +63,7 @@ type Query struct {
|
|||
HealthcheckTcp healthcheckTcp
|
||||
HealthcheckTcpHistory healthcheckTcpHistory
|
||||
OAuth2State oAuth2State
|
||||
Worker worker
|
||||
}
|
||||
|
||||
func (q *Query) Available() bool { return q.db != nil }
|
||||
|
@ -74,6 +78,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
|||
HealthcheckTcp: q.HealthcheckTcp.clone(db),
|
||||
HealthcheckTcpHistory: q.HealthcheckTcpHistory.clone(db),
|
||||
OAuth2State: q.OAuth2State.clone(db),
|
||||
Worker: q.Worker.clone(db),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,6 +100,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
|||
HealthcheckTcp: q.HealthcheckTcp.replaceDB(db),
|
||||
HealthcheckTcpHistory: q.HealthcheckTcpHistory.replaceDB(db),
|
||||
OAuth2State: q.OAuth2State.replaceDB(db),
|
||||
Worker: q.Worker.replaceDB(db),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,6 +112,7 @@ type queryCtx struct {
|
|||
HealthcheckTcp IHealthcheckTcpDo
|
||||
HealthcheckTcpHistory IHealthcheckTcpHistoryDo
|
||||
OAuth2State IOAuth2StateDo
|
||||
Worker IWorkerDo
|
||||
}
|
||||
|
||||
func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
||||
|
@ -117,6 +124,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
|||
HealthcheckTcp: q.HealthcheckTcp.WithContext(ctx),
|
||||
HealthcheckTcpHistory: q.HealthcheckTcpHistory.WithContext(ctx),
|
||||
OAuth2State: q.OAuth2State.WithContext(ctx),
|
||||
Worker: q.Worker.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
412
internal/models/query/workers.gen.go
Normal file
412
internal/models/query/workers.gen.go
Normal file
|
@ -0,0 +1,412 @@
|
|||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
|
||||
"gorm.io/plugin/dbresolver"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/models"
|
||||
)
|
||||
|
||||
func newWorker(db *gorm.DB, opts ...gen.DOOption) worker {
|
||||
_worker := worker{}
|
||||
|
||||
_worker.workerDo.UseDB(db, opts...)
|
||||
_worker.workerDo.UseModel(&models.Worker{})
|
||||
|
||||
tableName := _worker.workerDo.TableName()
|
||||
_worker.ALL = field.NewAsterisk(tableName)
|
||||
_worker.ID = field.NewUint(tableName, "id")
|
||||
_worker.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_worker.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_worker.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||
_worker.Name = field.NewString(tableName, "name")
|
||||
_worker.Slug = field.NewString(tableName, "slug")
|
||||
_worker.Status = field.NewString(tableName, "status")
|
||||
|
||||
_worker.fillFieldMap()
|
||||
|
||||
return _worker
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
workerDo workerDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.Uint
|
||||
CreatedAt field.Time
|
||||
UpdatedAt field.Time
|
||||
DeletedAt field.Field
|
||||
Name field.String
|
||||
Slug field.String
|
||||
Status field.String
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
|
||||
func (w worker) Table(newTableName string) *worker {
|
||||
w.workerDo.UseTable(newTableName)
|
||||
return w.updateTableName(newTableName)
|
||||
}
|
||||
|
||||
func (w worker) As(alias string) *worker {
|
||||
w.workerDo.DO = *(w.workerDo.As(alias).(*gen.DO))
|
||||
return w.updateTableName(alias)
|
||||
}
|
||||
|
||||
func (w *worker) updateTableName(table string) *worker {
|
||||
w.ALL = field.NewAsterisk(table)
|
||||
w.ID = field.NewUint(table, "id")
|
||||
w.CreatedAt = field.NewTime(table, "created_at")
|
||||
w.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
w.DeletedAt = field.NewField(table, "deleted_at")
|
||||
w.Name = field.NewString(table, "name")
|
||||
w.Slug = field.NewString(table, "slug")
|
||||
w.Status = field.NewString(table, "status")
|
||||
|
||||
w.fillFieldMap()
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *worker) WithContext(ctx context.Context) IWorkerDo { return w.workerDo.WithContext(ctx) }
|
||||
|
||||
func (w worker) TableName() string { return w.workerDo.TableName() }
|
||||
|
||||
func (w worker) Alias() string { return w.workerDo.Alias() }
|
||||
|
||||
func (w worker) Columns(cols ...field.Expr) gen.Columns { return w.workerDo.Columns(cols...) }
|
||||
|
||||
func (w *worker) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
_f, ok := w.fieldMap[fieldName]
|
||||
if !ok || _f == nil {
|
||||
return nil, false
|
||||
}
|
||||
_oe, ok := _f.(field.OrderExpr)
|
||||
return _oe, ok
|
||||
}
|
||||
|
||||
func (w *worker) fillFieldMap() {
|
||||
w.fieldMap = make(map[string]field.Expr, 7)
|
||||
w.fieldMap["id"] = w.ID
|
||||
w.fieldMap["created_at"] = w.CreatedAt
|
||||
w.fieldMap["updated_at"] = w.UpdatedAt
|
||||
w.fieldMap["deleted_at"] = w.DeletedAt
|
||||
w.fieldMap["name"] = w.Name
|
||||
w.fieldMap["slug"] = w.Slug
|
||||
w.fieldMap["status"] = w.Status
|
||||
}
|
||||
|
||||
func (w worker) clone(db *gorm.DB) worker {
|
||||
w.workerDo.ReplaceConnPool(db.Statement.ConnPool)
|
||||
return w
|
||||
}
|
||||
|
||||
func (w worker) replaceDB(db *gorm.DB) worker {
|
||||
w.workerDo.ReplaceDB(db)
|
||||
return w
|
||||
}
|
||||
|
||||
type workerDo struct{ gen.DO }
|
||||
|
||||
type IWorkerDo interface {
|
||||
gen.SubQuery
|
||||
Debug() IWorkerDo
|
||||
WithContext(ctx context.Context) IWorkerDo
|
||||
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
|
||||
ReplaceDB(db *gorm.DB)
|
||||
ReadDB() IWorkerDo
|
||||
WriteDB() IWorkerDo
|
||||
As(alias string) gen.Dao
|
||||
Session(config *gorm.Session) IWorkerDo
|
||||
Columns(cols ...field.Expr) gen.Columns
|
||||
Clauses(conds ...clause.Expression) IWorkerDo
|
||||
Not(conds ...gen.Condition) IWorkerDo
|
||||
Or(conds ...gen.Condition) IWorkerDo
|
||||
Select(conds ...field.Expr) IWorkerDo
|
||||
Where(conds ...gen.Condition) IWorkerDo
|
||||
Order(conds ...field.Expr) IWorkerDo
|
||||
Distinct(cols ...field.Expr) IWorkerDo
|
||||
Omit(cols ...field.Expr) IWorkerDo
|
||||
Join(table schema.Tabler, on ...field.Expr) IWorkerDo
|
||||
LeftJoin(table schema.Tabler, on ...field.Expr) IWorkerDo
|
||||
RightJoin(table schema.Tabler, on ...field.Expr) IWorkerDo
|
||||
Group(cols ...field.Expr) IWorkerDo
|
||||
Having(conds ...gen.Condition) IWorkerDo
|
||||
Limit(limit int) IWorkerDo
|
||||
Offset(offset int) IWorkerDo
|
||||
Count() (count int64, err error)
|
||||
Scopes(funcs ...func(gen.Dao) gen.Dao) IWorkerDo
|
||||
Unscoped() IWorkerDo
|
||||
Create(values ...*models.Worker) error
|
||||
CreateInBatches(values []*models.Worker, batchSize int) error
|
||||
Save(values ...*models.Worker) error
|
||||
First() (*models.Worker, error)
|
||||
Take() (*models.Worker, error)
|
||||
Last() (*models.Worker, error)
|
||||
Find() ([]*models.Worker, error)
|
||||
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.Worker, err error)
|
||||
FindInBatches(result *[]*models.Worker, batchSize int, fc func(tx gen.Dao, batch int) error) error
|
||||
Pluck(column field.Expr, dest interface{}) error
|
||||
Delete(...*models.Worker) (info gen.ResultInfo, err error)
|
||||
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
|
||||
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
|
||||
Updates(value interface{}) (info gen.ResultInfo, err error)
|
||||
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
|
||||
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
|
||||
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
|
||||
UpdateFrom(q gen.SubQuery) gen.Dao
|
||||
Attrs(attrs ...field.AssignExpr) IWorkerDo
|
||||
Assign(attrs ...field.AssignExpr) IWorkerDo
|
||||
Joins(fields ...field.RelationField) IWorkerDo
|
||||
Preload(fields ...field.RelationField) IWorkerDo
|
||||
FirstOrInit() (*models.Worker, error)
|
||||
FirstOrCreate() (*models.Worker, error)
|
||||
FindByPage(offset int, limit int) (result []*models.Worker, count int64, err error)
|
||||
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
|
||||
Scan(result interface{}) (err error)
|
||||
Returning(value interface{}, columns ...string) IWorkerDo
|
||||
UnderlyingDB() *gorm.DB
|
||||
schema.Tabler
|
||||
}
|
||||
|
||||
func (w workerDo) Debug() IWorkerDo {
|
||||
return w.withDO(w.DO.Debug())
|
||||
}
|
||||
|
||||
func (w workerDo) WithContext(ctx context.Context) IWorkerDo {
|
||||
return w.withDO(w.DO.WithContext(ctx))
|
||||
}
|
||||
|
||||
func (w workerDo) ReadDB() IWorkerDo {
|
||||
return w.Clauses(dbresolver.Read)
|
||||
}
|
||||
|
||||
func (w workerDo) WriteDB() IWorkerDo {
|
||||
return w.Clauses(dbresolver.Write)
|
||||
}
|
||||
|
||||
func (w workerDo) Session(config *gorm.Session) IWorkerDo {
|
||||
return w.withDO(w.DO.Session(config))
|
||||
}
|
||||
|
||||
func (w workerDo) Clauses(conds ...clause.Expression) IWorkerDo {
|
||||
return w.withDO(w.DO.Clauses(conds...))
|
||||
}
|
||||
|
||||
func (w workerDo) Returning(value interface{}, columns ...string) IWorkerDo {
|
||||
return w.withDO(w.DO.Returning(value, columns...))
|
||||
}
|
||||
|
||||
func (w workerDo) Not(conds ...gen.Condition) IWorkerDo {
|
||||
return w.withDO(w.DO.Not(conds...))
|
||||
}
|
||||
|
||||
func (w workerDo) Or(conds ...gen.Condition) IWorkerDo {
|
||||
return w.withDO(w.DO.Or(conds...))
|
||||
}
|
||||
|
||||
func (w workerDo) Select(conds ...field.Expr) IWorkerDo {
|
||||
return w.withDO(w.DO.Select(conds...))
|
||||
}
|
||||
|
||||
func (w workerDo) Where(conds ...gen.Condition) IWorkerDo {
|
||||
return w.withDO(w.DO.Where(conds...))
|
||||
}
|
||||
|
||||
func (w workerDo) Order(conds ...field.Expr) IWorkerDo {
|
||||
return w.withDO(w.DO.Order(conds...))
|
||||
}
|
||||
|
||||
func (w workerDo) Distinct(cols ...field.Expr) IWorkerDo {
|
||||
return w.withDO(w.DO.Distinct(cols...))
|
||||
}
|
||||
|
||||
func (w workerDo) Omit(cols ...field.Expr) IWorkerDo {
|
||||
return w.withDO(w.DO.Omit(cols...))
|
||||
}
|
||||
|
||||
func (w workerDo) Join(table schema.Tabler, on ...field.Expr) IWorkerDo {
|
||||
return w.withDO(w.DO.Join(table, on...))
|
||||
}
|
||||
|
||||
func (w workerDo) LeftJoin(table schema.Tabler, on ...field.Expr) IWorkerDo {
|
||||
return w.withDO(w.DO.LeftJoin(table, on...))
|
||||
}
|
||||
|
||||
func (w workerDo) RightJoin(table schema.Tabler, on ...field.Expr) IWorkerDo {
|
||||
return w.withDO(w.DO.RightJoin(table, on...))
|
||||
}
|
||||
|
||||
func (w workerDo) Group(cols ...field.Expr) IWorkerDo {
|
||||
return w.withDO(w.DO.Group(cols...))
|
||||
}
|
||||
|
||||
func (w workerDo) Having(conds ...gen.Condition) IWorkerDo {
|
||||
return w.withDO(w.DO.Having(conds...))
|
||||
}
|
||||
|
||||
func (w workerDo) Limit(limit int) IWorkerDo {
|
||||
return w.withDO(w.DO.Limit(limit))
|
||||
}
|
||||
|
||||
func (w workerDo) Offset(offset int) IWorkerDo {
|
||||
return w.withDO(w.DO.Offset(offset))
|
||||
}
|
||||
|
||||
func (w workerDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IWorkerDo {
|
||||
return w.withDO(w.DO.Scopes(funcs...))
|
||||
}
|
||||
|
||||
func (w workerDo) Unscoped() IWorkerDo {
|
||||
return w.withDO(w.DO.Unscoped())
|
||||
}
|
||||
|
||||
func (w workerDo) Create(values ...*models.Worker) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return w.DO.Create(values)
|
||||
}
|
||||
|
||||
func (w workerDo) CreateInBatches(values []*models.Worker, batchSize int) error {
|
||||
return w.DO.CreateInBatches(values, batchSize)
|
||||
}
|
||||
|
||||
// Save : !!! underlying implementation is different with GORM
|
||||
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
|
||||
func (w workerDo) Save(values ...*models.Worker) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return w.DO.Save(values)
|
||||
}
|
||||
|
||||
func (w workerDo) First() (*models.Worker, error) {
|
||||
if result, err := w.DO.First(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*models.Worker), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w workerDo) Take() (*models.Worker, error) {
|
||||
if result, err := w.DO.Take(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*models.Worker), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w workerDo) Last() (*models.Worker, error) {
|
||||
if result, err := w.DO.Last(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*models.Worker), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w workerDo) Find() ([]*models.Worker, error) {
|
||||
result, err := w.DO.Find()
|
||||
return result.([]*models.Worker), err
|
||||
}
|
||||
|
||||
func (w workerDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.Worker, err error) {
|
||||
buf := make([]*models.Worker, 0, batchSize)
|
||||
err = w.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
|
||||
defer func() { results = append(results, buf...) }()
|
||||
return fc(tx, batch)
|
||||
})
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (w workerDo) FindInBatches(result *[]*models.Worker, batchSize int, fc func(tx gen.Dao, batch int) error) error {
|
||||
return w.DO.FindInBatches(result, batchSize, fc)
|
||||
}
|
||||
|
||||
func (w workerDo) Attrs(attrs ...field.AssignExpr) IWorkerDo {
|
||||
return w.withDO(w.DO.Attrs(attrs...))
|
||||
}
|
||||
|
||||
func (w workerDo) Assign(attrs ...field.AssignExpr) IWorkerDo {
|
||||
return w.withDO(w.DO.Assign(attrs...))
|
||||
}
|
||||
|
||||
func (w workerDo) Joins(fields ...field.RelationField) IWorkerDo {
|
||||
for _, _f := range fields {
|
||||
w = *w.withDO(w.DO.Joins(_f))
|
||||
}
|
||||
return &w
|
||||
}
|
||||
|
||||
func (w workerDo) Preload(fields ...field.RelationField) IWorkerDo {
|
||||
for _, _f := range fields {
|
||||
w = *w.withDO(w.DO.Preload(_f))
|
||||
}
|
||||
return &w
|
||||
}
|
||||
|
||||
func (w workerDo) FirstOrInit() (*models.Worker, error) {
|
||||
if result, err := w.DO.FirstOrInit(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*models.Worker), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w workerDo) FirstOrCreate() (*models.Worker, error) {
|
||||
if result, err := w.DO.FirstOrCreate(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return result.(*models.Worker), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w workerDo) FindByPage(offset int, limit int) (result []*models.Worker, count int64, err error) {
|
||||
result, err = w.Offset(offset).Limit(limit).Find()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if size := len(result); 0 < limit && 0 < size && size < limit {
|
||||
count = int64(size + offset)
|
||||
return
|
||||
}
|
||||
|
||||
count, err = w.Offset(-1).Limit(-1).Count()
|
||||
return
|
||||
}
|
||||
|
||||
func (w workerDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
|
||||
count, err = w.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = w.Offset(offset).Limit(limit).Scan(result)
|
||||
return
|
||||
}
|
||||
|
||||
func (w workerDo) Scan(result interface{}) (err error) {
|
||||
return w.DO.Scan(result)
|
||||
}
|
||||
|
||||
func (w workerDo) Delete(models ...*models.Worker) (result gen.ResultInfo, err error) {
|
||||
return w.DO.Delete(models)
|
||||
}
|
||||
|
||||
func (w *workerDo) withDO(do gen.Dao) *workerDo {
|
||||
w.DO = *do.(*gen.DO)
|
||||
return w
|
||||
}
|
21
internal/services/worker.go
Normal file
21
internal/services/worker.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/models"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/models/query"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func CreateWorker(ctx context.Context, db *gorm.DB, worker *models.Worker) error {
|
||||
return db.WithContext(ctx).Create(worker).Error
|
||||
}
|
||||
|
||||
func GetWorker(ctx context.Context, q *query.Query, slug string) (*models.Worker, error) {
|
||||
log.Println("GetWorker")
|
||||
return q.Worker.WithContext(ctx).Where(
|
||||
q.Worker.Slug.Eq(slug),
|
||||
).First()
|
||||
}
|
|
@ -1,18 +1,52 @@
|
|||
package temporal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/jwt"
|
||||
"code.tjo.space/mentos1386/zdravko/pkg/retry"
|
||||
"go.temporal.io/sdk/client"
|
||||
)
|
||||
|
||||
func ConnectToTemporal(cfg *config.Config) (client.Client, error) {
|
||||
type AuthHeadersProvider struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
func (p *AuthHeadersProvider) GetHeaders(ctx context.Context) (map[string]string, error) {
|
||||
return map[string]string{
|
||||
"authorization": "Bearer " + p.Token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ConnectServerToTemporal(cfg *config.Config) (client.Client, error) {
|
||||
// For server we generate new token with admin permissions
|
||||
token, err := jwt.NewToken(cfg, []string{"temporal-system:admin", "default:admin"}, "server")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider := &AuthHeadersProvider{token}
|
||||
|
||||
// Try to connect to the Temporal Server
|
||||
return retry.Retry(5, 6*time.Second, func() (client.Client, error) {
|
||||
return client.Dial(client.Options{
|
||||
HostPort: cfg.Temporal.ServerHost,
|
||||
HostPort: cfg.Temporal.ServerHost,
|
||||
HeadersProvider: provider,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func ConnectWorkerToTemporal(cfg *config.Config) (client.Client, error) {
|
||||
provider := &AuthHeadersProvider{cfg.Worker.Token}
|
||||
|
||||
// Try to connect to the Temporal Server
|
||||
return retry.Retry(5, 6*time.Second, func() (client.Client, error) {
|
||||
return client.Dial(client.Options{
|
||||
HostPort: cfg.Temporal.ServerHost,
|
||||
HeadersProvider: provider,
|
||||
Namespace: "default",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
16
justfile
16
justfile
|
@ -3,6 +3,10 @@ set shell := ["devbox", "run"]
|
|||
# Load dotenv
|
||||
set dotenv-load
|
||||
|
||||
# Load public and private keys
|
||||
export JWT_PRIVATE_KEY := `cat jwt.private.pem`
|
||||
export JWT_PUBLIC_KEY := `cat jwt.public.pem`
|
||||
|
||||
GIT_SHA := `git rev-parse --short HEAD`
|
||||
DOCKER_IMAGE := "ghcr.io/mentos1386/zdravko:sha-"+GIT_SHA
|
||||
STATIC_DIR := "./web/static"
|
||||
|
@ -11,6 +15,7 @@ STATIC_DIR := "./web/static"
|
|||
build:
|
||||
docker build -f build/Dockerfile -t {{DOCKER_IMAGE}} .
|
||||
|
||||
# Run Docker application.
|
||||
run-docker:
|
||||
docker run -p 8080:8080 \
|
||||
-e SESSION_SECRET \
|
||||
|
@ -26,10 +31,19 @@ run-docker:
|
|||
run:
|
||||
devbox services up
|
||||
|
||||
run-worker:
|
||||
go build -o dist/zdravko cmd/zdravko/main.go
|
||||
./dist/zdravko --worker=true --server=false --temporal=false
|
||||
|
||||
# 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
|
||||
|
||||
# Start zdravko
|
||||
run-zdravko:
|
||||
go build -o dist/zdravko cmd/zdravko/main.go
|
||||
./dist/zdravko
|
||||
./dist/zdravko --worker=false
|
||||
|
||||
# Deploy the application to fly.io
|
||||
deploy:
|
||||
|
|
|
@ -37,10 +37,11 @@ func (s *Server) Start() error {
|
|||
}
|
||||
log.Println("Connected to database")
|
||||
|
||||
temporalClient, err := temporal.ConnectToTemporal(s.cfg)
|
||||
temporalClient, err := temporal.ConnectServerToTemporal(s.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("Connected to Temporal")
|
||||
|
||||
h := handlers.NewBaseHandler(db, query, temporalClient, s.cfg)
|
||||
|
||||
|
@ -71,6 +72,11 @@ func (s *Server) Start() error {
|
|||
r.HandleFunc("/settings/healthchecks/create", h.Authenticated(h.SettingsHealthchecksCreateGET)).Methods("GET")
|
||||
r.HandleFunc("/settings/healthchecks/create", h.Authenticated(h.SettingsHealthchecksCreatePOST)).Methods("POST")
|
||||
r.HandleFunc("/settings/healthchecks/{slug}", h.Authenticated(h.SettingsHealthchecksDescribeGET)).Methods("GET")
|
||||
r.HandleFunc("/settings/workers", h.Authenticated(h.SettingsWorkersGET)).Methods("GET")
|
||||
r.HandleFunc("/settings/workers/create", h.Authenticated(h.SettingsWorkersCreateGET)).Methods("GET")
|
||||
r.HandleFunc("/settings/workers/create", h.Authenticated(h.SettingsWorkersCreatePOST)).Methods("POST")
|
||||
r.HandleFunc("/settings/workers/{slug}", h.Authenticated(h.SettingsWorkersDescribeGET)).Methods("GET")
|
||||
r.HandleFunc("/settings/workers/{slug}/token", h.Authenticated(h.SettingsWorkersTokenGET)).Methods("GET")
|
||||
|
||||
// OAuth2
|
||||
r.HandleFunc("/oauth2/login", h.OAuth2LoginGET).Methods("GET")
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package temporal
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
internal "code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/jwt"
|
||||
"go.temporal.io/server/common/cluster"
|
||||
"go.temporal.io/server/common/config"
|
||||
"go.temporal.io/server/common/persistence/sql/sqlplugin/sqlite"
|
||||
|
@ -20,6 +23,29 @@ const HistoryPort = 7234
|
|||
const MatchingPort = 7235
|
||||
const WorkerPort = 7236
|
||||
|
||||
type TokenKeyProvider struct {
|
||||
config *internal.Config
|
||||
}
|
||||
|
||||
func (p *TokenKeyProvider) SupportedMethods() []string {
|
||||
return []string{"RS256", "RS384", "RS512"}
|
||||
}
|
||||
|
||||
func (p *TokenKeyProvider) HmacKey(alg string, kid string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("HMAC key is not supported")
|
||||
}
|
||||
|
||||
func (p *TokenKeyProvider) EcdsaKey(alg string, kid string) (*ecdsa.PublicKey, error) {
|
||||
return nil, fmt.Errorf("ECDSA key is not supported")
|
||||
}
|
||||
|
||||
func (p *TokenKeyProvider) RsaKey(alg string, kid string) (*rsa.PublicKey, error) {
|
||||
return jwt.JwtPublicKey(p.config)
|
||||
}
|
||||
|
||||
func (p *TokenKeyProvider) Close() {
|
||||
}
|
||||
|
||||
func NewServerConfig(cfg *internal.Config) *config.Config {
|
||||
return &config.Config{
|
||||
Persistence: config.Persistence{
|
||||
|
@ -42,6 +68,7 @@ func NewServerConfig(cfg *internal.Config) *config.Config {
|
|||
MaxJoinDuration: 30 * time.Second,
|
||||
BroadcastAddress: BroadcastAddress,
|
||||
},
|
||||
Authorization: config.Authorization{},
|
||||
},
|
||||
Services: map[string]config.Service{
|
||||
"frontend": {
|
||||
|
@ -69,14 +96,6 @@ func NewServerConfig(cfg *internal.Config) *config.Config {
|
|||
BindOnIP: "",
|
||||
},
|
||||
},
|
||||
"worker": {
|
||||
RPC: config.RPC{
|
||||
GRPCPort: WorkerPort,
|
||||
MembershipPort: WorkerPort + 100,
|
||||
BindOnLocalHost: true,
|
||||
BindOnIP: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
ClusterMetadata: &cluster.Config{
|
||||
EnableGlobalNamespace: false,
|
||||
|
|
|
@ -8,11 +8,12 @@ import (
|
|||
"go.temporal.io/server/common/authorization"
|
||||
"go.temporal.io/server/common/config"
|
||||
"go.temporal.io/server/common/log"
|
||||
"go.temporal.io/server/common/primitives"
|
||||
"go.temporal.io/server/schema/sqlite"
|
||||
t "go.temporal.io/server/temporal"
|
||||
)
|
||||
|
||||
func NewServer(cfg *config.Config) (t.Server, error) {
|
||||
func NewServer(cfg *config.Config, tokenKeyProvider authorization.TokenKeyProvider) (t.Server, error) {
|
||||
logger := log.NewZapLogger(log.BuildZapLogger(log.Config{
|
||||
Stdout: true,
|
||||
Level: "info",
|
||||
|
@ -43,15 +44,8 @@ func NewServer(cfg *config.Config) (t.Server, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
authorizer, err := authorization.GetAuthorizerFromConfig(&cfg.Global.Authorization)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claimMapper, err := authorization.GetClaimMapperFromConfig(&cfg.Global.Authorization, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorizer := authorization.NewDefaultAuthorizer()
|
||||
claimMapper := authorization.NewDefaultJWTClaimMapper(tokenKeyProvider, &cfg.Global.Authorization, logger)
|
||||
|
||||
ctx := context.Background()
|
||||
interruptChan := make(chan interface{}, 1)
|
||||
|
@ -67,12 +61,16 @@ func NewServer(cfg *config.Config) (t.Server, error) {
|
|||
|
||||
return t.NewServer(
|
||||
t.WithConfig(cfg),
|
||||
t.ForServices(t.DefaultServices),
|
||||
t.ForServices([]string{
|
||||
string(primitives.FrontendService),
|
||||
string(primitives.HistoryService),
|
||||
string(primitives.MatchingService),
|
||||
}),
|
||||
t.WithLogger(logger),
|
||||
t.InterruptOn(interruptChan),
|
||||
t.WithAuthorizer(authorizer),
|
||||
t.WithClaimMapper(func(cfg *config.Config) authorization.ClaimMapper {
|
||||
return claimMapper
|
||||
}),
|
||||
t.InterruptOn(interruptChan),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,8 @@ type Temporal struct {
|
|||
|
||||
func NewTemporal(cfg *config.Config) (*Temporal, error) {
|
||||
serverConfig := NewServerConfig(cfg)
|
||||
server, err := NewServer(serverConfig)
|
||||
tokenKeyProvider := TokenKeyProvider{config: cfg}
|
||||
server, err := NewServer(serverConfig, &tokenKeyProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -45,11 +46,12 @@ func (t *Temporal) Start() error {
|
|||
}
|
||||
|
||||
func (t *Temporal) Stop() error {
|
||||
t.uiServer.Stop()
|
||||
|
||||
err := t.server.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.uiServer.Stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -24,14 +24,14 @@ func (w *Worker) Name() string {
|
|||
}
|
||||
|
||||
func (w *Worker) Start() error {
|
||||
temporalClient, err := temporal.ConnectToTemporal(w.cfg)
|
||||
temporalClient, err := temporal.ConnectWorkerToTemporal(w.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new Worker
|
||||
// TODO: Maybe identify by region or something?
|
||||
w.worker = worker.New(temporalClient, "default", worker.Options{})
|
||||
w.worker = worker.New(temporalClient, "test", worker.Options{})
|
||||
|
||||
// Register Workflows
|
||||
w.worker.RegisterWorkflow(workflows.HealthcheckHttpWorkflowDefinition)
|
||||
|
|
|
@ -29,6 +29,7 @@ func main() {
|
|||
|
||||
// Generate default DAO interface for those specified structs
|
||||
g.ApplyBasic(
|
||||
models.Worker{},
|
||||
models.HealthcheckHttp{},
|
||||
models.HealthcheckHttpHistory{},
|
||||
models.HealthcheckTcp{},
|
||||
|
|
|
@ -26,8 +26,7 @@
|
|||
List of Healthchecks
|
||||
<div class="mt-1 flex">
|
||||
<p class="mt-1 text-sm font-normal text-gray-500">
|
||||
Healthchecks represent periodic checks of some HTTP or TCP service, to see if it's
|
||||
responding correctly to deterime if it's healthy or not.
|
||||
{{ $description }}
|
||||
</p>
|
||||
<a href="/settings/healthchecks/create" class="inline-flex justify-center items-center py-1 px-2 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">
|
||||
Create New
|
||||
|
|
68
web/templates/pages/settings_workers.tmpl
Normal file
68
web/templates/pages/settings_workers.tmpl
Normal file
|
@ -0,0 +1,68 @@
|
|||
{{define "settings"}}
|
||||
|
||||
{{ $description := "Workers are executing healthchecks. You can deploy multiple of thems to multiple regions for wider coverage." }}
|
||||
|
||||
{{ if eq .WorkersLength 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 workers yet.
|
||||
</h1>
|
||||
<p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 lg:px-40">
|
||||
{{ $description }}
|
||||
</p>
|
||||
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
|
||||
<a href="/settings/workers/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 Worker
|
||||
<svg class="feather ml-1 h-5 w-5 overflow-visible"><use href="/static/icons/feather-sprite.svg#plus" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{ else }}
|
||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
|
||||
<caption class="p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white">
|
||||
List of Workers
|
||||
<div class="mt-1 flex">
|
||||
<p class="mt-1 text-sm font-normal text-gray-500">
|
||||
{{ $description }}
|
||||
</p>
|
||||
<a href="/settings/workers/create" class="inline-flex justify-center items-center py-1 px-2 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">
|
||||
Create New
|
||||
<svg class="feather h-5 w-5 overflow-visible"><use href="/static/icons/feather-sprite.svg#plus" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
</caption>
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{range .Workers}}
|
||||
<tbody>
|
||||
<tr class="odd:bg-white even:bg-gray-50">
|
||||
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
{{.Name}}
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
OK
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<a href="/settings/workers/{{.Slug}}" class="font-medium text-blue-600 hover:underline">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
14
web/templates/pages/settings_workers_create.tmpl
Normal file
14
web/templates/pages/settings_workers_create.tmpl
Normal file
|
@ -0,0 +1,14 @@
|
|||
{{define "settings"}}
|
||||
<section class="relative overflow-x-auto shadow-md sm:rounded-lg p-5 text-gray-500 bg-white">
|
||||
<h1 class="text-lg font-semibold text-gray-900">
|
||||
Creating new worker.
|
||||
</h1>
|
||||
<form class="max-w-sm mt-4" action="/settings/workers/create" method="post">
|
||||
<div class="mb-5">
|
||||
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">Name</label>
|
||||
<input type="name" name="name" id="name" placeholder="FooBar" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"/>
|
||||
</div>
|
||||
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center">Create</button>
|
||||
</form>
|
||||
</section>
|
||||
{{end}}
|
27
zdravko.yaml
27
zdravko.yaml
|
@ -1,27 +0,0 @@
|
|||
healthchecks:
|
||||
- name: "Google"
|
||||
http:
|
||||
url: "https://www.google.com"
|
||||
method: GET
|
||||
timeout: 5s
|
||||
schedule: "* * * * *"
|
||||
retries: 3
|
||||
- name: "GitHub"
|
||||
http:
|
||||
url: "https://www.github.com"
|
||||
method: GET
|
||||
timeout: 5s
|
||||
schedule: "* * * * *"
|
||||
retries: 3
|
||||
- name: "Docker"
|
||||
tcp:
|
||||
hostname: "docker.com"
|
||||
port: 443
|
||||
schedule: "* * * * *"
|
||||
timeout: 60s
|
||||
retries: 3
|
||||
|
||||
cronjobs:
|
||||
- name: "Backup"
|
||||
schedule: "0 0 * * *"
|
||||
buffer: 1h
|
Loading…
Reference in a new issue