feat(workers): initial worker authentication and provisioning

This commit is contained in:
Tine 2024-02-18 22:37:17 +01:00
parent 2b6e1ca09b
commit 478232bcda
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
29 changed files with 916 additions and 103 deletions

8
.github/workflows/deploy.yaml vendored Normal file
View file

@ -0,0 +1,8 @@
name: Deploy
on:
push:
branches:
- main
jobs:

3
.gitignore vendored
View file

@ -5,5 +5,8 @@ zdravko.db
temporal.db
temporal.db-journal
# Keys
*.pem
# Config
.env

View file

@ -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
```

View file

@ -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

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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 {

View file

@ -15,6 +15,7 @@ func ConnectToDatabase(path string) (*gorm.DB, *query.Query, error) {
}
err = db.AutoMigrate(
models.Worker{},
models.HealthcheckHttp{},
models.HealthcheckHttpHistory{},
models.HealthcheckTcp{},

View file

@ -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"},
}

View file

@ -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,

View 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
View 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
}

View file

@ -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 {

View file

@ -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),
}
}

View 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
}

View 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()
}

View file

@ -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",
})
})
}

View file

@ -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:

View file

@ -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")

View file

@ -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,

View file

@ -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),
)
}

View file

@ -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
}

View file

@ -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)

View file

@ -29,6 +29,7 @@ func main() {
// Generate default DAO interface for those specified structs
g.ApplyBasic(
models.Worker{},
models.HealthcheckHttp{},
models.HealthcheckHttpHistory{},
models.HealthcheckTcp{},

View file

@ -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

View 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}}

View 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}}

View file

@ -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