mirror of
https://github.com/mentos1386/zdravko.git
synced 2024-11-21 23:33:34 +00:00
feat: single binary
This commit is contained in:
parent
97b2d86cbf
commit
0db0f3e6b8
16 changed files with 400 additions and 142 deletions
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
build
|
||||
deploy
|
||||
docs
|
55
.github/workflows/build.yaml
vendored
Normal file
55
.github/workflows/build.yaml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
name: Build
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 10 * * *"
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/mentos1386/zdravko
|
||||
tags: |
|
||||
type=schedule
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
38
build/Dockerfile
Normal file
38
build/Dockerfile
Normal file
|
@ -0,0 +1,38 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
ARG GO_VERSION=1.21.0
|
||||
FROM golang:${GO_VERSION} as builder
|
||||
# Set destination for COPY
|
||||
WORKDIR /app
|
||||
|
||||
# Download Go modules
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . ./
|
||||
|
||||
# Build
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -o /zdravko cmd/zdravko/main.go
|
||||
|
||||
###
|
||||
# Final production
|
||||
FROM gcr.io/distroless/base-debian12:nonroot as production
|
||||
COPY --from=builder /zdravko /zdravko
|
||||
COPY LICENSE /LICENSE
|
||||
COPY README.md /README.md
|
||||
|
||||
# Zdravko Server
|
||||
ENV PORT=8080
|
||||
EXPOSE 8080
|
||||
# Temporal UI Server
|
||||
EXPOSE 8223
|
||||
# Temporal GRPC Server
|
||||
EXPOSE 7233
|
||||
|
||||
# Volume to persist sqlite databases
|
||||
VOLUME /data
|
||||
|
||||
ENV DATABASE_PATH=/data/zdravko.db
|
||||
ENV TEMPORAL_DATABASE_PATH=/data/temporal.db
|
||||
|
||||
ENTRYPOINT ["/zdravko"]
|
|
@ -1,52 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
t "code.tjo.space/mentos1386/zdravko/pkg/temporal"
|
||||
)
|
||||
|
||||
func backendServer(config *config.Config) {
|
||||
serverConfig := t.NewServerConfig(config)
|
||||
|
||||
server, err := t.NewServer(serverConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create server: %v", err)
|
||||
}
|
||||
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to start server: %v", err)
|
||||
}
|
||||
|
||||
err = server.Stop()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to stop server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func frontendServer(config *config.Config) {
|
||||
uiConfig := t.NewUiConfig(config)
|
||||
|
||||
uiServer, err := t.NewUiServer(uiConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create UI server: %v", err)
|
||||
}
|
||||
|
||||
err = uiServer.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to start UI server: %v", err)
|
||||
}
|
||||
|
||||
uiServer.Stop()
|
||||
}
|
||||
|
||||
func main() {
|
||||
config := config.NewConfig()
|
||||
|
||||
go func() {
|
||||
frontendServer(config)
|
||||
}()
|
||||
backendServer(config)
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/activities"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/workflows"
|
||||
"go.temporal.io/sdk/worker"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.NewConfig()
|
||||
|
||||
temporalClient, err := internal.ConnectToTemporal(cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer temporalClient.Close()
|
||||
|
||||
// Create a new Worker
|
||||
// TODO: Maybe identify by region or something?
|
||||
w := worker.New(temporalClient, "default", worker.Options{})
|
||||
|
||||
// Register Workflows
|
||||
w.RegisterWorkflow(workflows.HealthcheckHttpWorkflowDefinition)
|
||||
|
||||
// Register Activities
|
||||
w.RegisterActivity(activities.HealthcheckHttpActivityDefinition)
|
||||
|
||||
// Start the the Worker Process
|
||||
err = w.Run(worker.InterruptCh())
|
||||
if err != nil {
|
||||
log.Fatalln("Unable to start the Worker Process", err)
|
||||
}
|
||||
}
|
99
cmd/zdravko/main.go
Normal file
99
cmd/zdravko/main.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"code.tjo.space/mentos1386/zdravko/pkg/server"
|
||||
"code.tjo.space/mentos1386/zdravko/pkg/temporal"
|
||||
"code.tjo.space/mentos1386/zdravko/pkg/worker"
|
||||
)
|
||||
|
||||
type StartableAndStoppable interface {
|
||||
Name() string
|
||||
Start() error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
func main() {
|
||||
var startServer bool
|
||||
var startWorker bool
|
||||
var startTemporal bool
|
||||
|
||||
flag.BoolVar(&startServer, "server", true, "Start the server")
|
||||
flag.BoolVar(&startWorker, "worker", true, "Start the worker")
|
||||
flag.BoolVar(&startTemporal, "temporal", true, "Start the temporal")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
println("Starting zdravko...")
|
||||
println("Server: ", startServer)
|
||||
println("Worker: ", startWorker)
|
||||
println("Temporal: ", startTemporal)
|
||||
|
||||
cfg := config.NewConfig()
|
||||
|
||||
var servers [3]StartableAndStoppable
|
||||
var wg sync.WaitGroup
|
||||
|
||||
if startTemporal {
|
||||
log.Println("Setting up Temporal")
|
||||
temporal, err := temporal.NewTemporal(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create temporal: %v", err)
|
||||
}
|
||||
servers[0] = temporal
|
||||
}
|
||||
|
||||
if startServer {
|
||||
log.Println("Setting up Server")
|
||||
server, err := server.NewServer(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create server: %v", err)
|
||||
}
|
||||
servers[1] = server
|
||||
}
|
||||
|
||||
if startWorker {
|
||||
log.Println("Setting up Worker")
|
||||
worker, err := worker.NewWorker(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create worker: %v", err)
|
||||
}
|
||||
servers[2] = worker
|
||||
}
|
||||
|
||||
for _, s := range servers {
|
||||
srv := s
|
||||
println("Starting", srv.Name())
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := srv.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to start server %s: %v", srv.Name(), err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
log.Printf("Received signal: %v", sig)
|
||||
for _, s := range servers {
|
||||
println("Stopping", s.Name())
|
||||
err := s.Stop()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to stop server %s: %v", s.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
11
deploy/Dockerfile
Normal file
11
deploy/Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
ARG ALPINE_VERSION=3.14
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} as temporal
|
||||
RUN apk add ca-certificates fuse3 sqlite
|
||||
COPY --from=flyio/litefs:0.5 /usr/local/bin/litefs /usr/local/bin/litefs
|
||||
COPY litefs.yaml /etc/litefs.yaml
|
||||
|
||||
COPY --from=ghcr.io/mentos1386/zdravko /zdravko /zdravko
|
||||
|
||||
ENTRYPOINT litefs mount
|
|
@ -4,32 +4,26 @@ app = 'zdravko'
|
|||
primary_region = 'waw'
|
||||
|
||||
[build]
|
||||
builder = 'paketobuildpacks/builder:base'
|
||||
buildpacks = ['gcr.io/paketo-buildpacks/go']
|
||||
dockerfile = "Dockerfile"
|
||||
|
||||
[env]
|
||||
PORT = '8080'
|
||||
ROOT_URL = 'https://zdravko.mnts.dev'
|
||||
DATABASE_PATH = '/data/zdravko.db'
|
||||
|
||||
OAUTH2_ENDPOINT_TOKEN_URL = 'https://github.com/login/oauth/access_token'
|
||||
OAUTH2_ENDPOINT_AUTH_URL = 'https://github.com/login/oauth/authorize'
|
||||
OAUTH2_ENDPOINT_USER_INFO_URL = 'https://api.github.com/user'
|
||||
|
||||
TEMPORAL_DATABASE_PATH = '/data/temporal.db'
|
||||
TEMPORAL_UI_HOST = 'temporal.process.zdravko.internal:8223'
|
||||
TEMPORAL_SERVER_HOST = 'temporal.process.zdravko.internal:7233'
|
||||
TEMPORAL_UI_HOST = 'server.process.zdravko.internal:8223'
|
||||
TEMPORAL_SERVER_HOST = 'server.process.zdravko.internal:7233'
|
||||
|
||||
[processes]
|
||||
server = "server"
|
||||
worker = "worker"
|
||||
temporal = "temporal"
|
||||
server = "zdravko --temporal=false --server=true --worker=false"
|
||||
worker = "zdravko --temporal=false --server=false --worker=true"
|
||||
|
||||
[[mounts]]
|
||||
source = "zdravko_data"
|
||||
destination = "/data"
|
||||
processes = ["server", "temporal"]
|
||||
initial_size = "3GB"
|
||||
source = "litefs"
|
||||
destination = "/var/lib/litefs"
|
||||
processes = ["server"]
|
||||
|
||||
[http_service]
|
||||
processes = ["server"]
|
35
deploy/litefs.yaml
Normal file
35
deploy/litefs.yaml
Normal file
|
@ -0,0 +1,35 @@
|
|||
# This directory is where your application will access the database.
|
||||
fuse:
|
||||
dir: "/data"
|
||||
|
||||
# This directory is where LiteFS will store internal data.
|
||||
# You must place this directory on a persistent volume.
|
||||
data:
|
||||
dir: "/var/lib/litefs"
|
||||
|
||||
exec:
|
||||
- cmd: "/zdravko --temporal=true --server=true --worker=false"
|
||||
|
||||
lease:
|
||||
type: "consul"
|
||||
|
||||
# Specifies if this node can become primary. The expression below evaluates
|
||||
# to true on nodes that are run in the primary region. Nodes in other regions
|
||||
# act as non-candidate, read-only replicas.
|
||||
candidate: ${FLY_REGION == PRIMARY_REGION}
|
||||
|
||||
# If true, then the node will automatically become primary after it has
|
||||
# connected with the cluster and sync'd up. This makes it easier to run
|
||||
# migrations on start up.
|
||||
promote: true
|
||||
|
||||
# The API URL that other nodes will use to connect to this node.
|
||||
advertise-url: "http://${FLY_ALLOC_ID}.vm.${FLY_APP_NAME}.internal:20202"
|
||||
|
||||
consul:
|
||||
# The URL of the Consul cluster.
|
||||
url: "${FLY_CONSUL_URL}"
|
||||
|
||||
# A unique key shared by all nodes in the LiteFS cluster.
|
||||
# Change this if you are running multiple clusters in a single app!
|
||||
key: "${FLY_APP_NAME}/primary"
|
|
@ -1,4 +1,4 @@
|
|||
package internal
|
||||
package temporal
|
||||
|
||||
import (
|
||||
"time"
|
26
justfile
26
justfile
|
@ -5,28 +5,22 @@ set dotenv-load
|
|||
|
||||
STATIC_DIR := "./web/static"
|
||||
|
||||
# Build the application
|
||||
build:
|
||||
docker build -f build/Dockerfile -t ghcr.io/mentos1386/zdravko:latest .
|
||||
|
||||
# Run full development environment
|
||||
run:
|
||||
devbox services up
|
||||
|
||||
# Start temporal which is accassible at http://localhost:8233/
|
||||
run-temporal:
|
||||
go build -o dist/temporal cmd/temporal/main.go
|
||||
./dist/temporal
|
||||
|
||||
# Start web server accessible at http://localhost:8080/
|
||||
run-server:
|
||||
go build -o dist/server cmd/server/main.go
|
||||
./dist/server
|
||||
|
||||
# Run worker
|
||||
run-worker:
|
||||
go build -o dist/worker cmd/worker/main.go
|
||||
./dist/worker
|
||||
# Start zdravko
|
||||
run-zdravko:
|
||||
go build -o dist/zdravko cmd/zdravko/main.go
|
||||
./dist/zdravko
|
||||
|
||||
# Deploy the application to fly.io
|
||||
deploy:
|
||||
fly deploy
|
||||
fly deploy -c deploy/fly.toml
|
||||
|
||||
# Start devbox shell
|
||||
shell:
|
||||
|
@ -38,7 +32,7 @@ generate:
|
|||
go generate ./...
|
||||
|
||||
_tailwindcss-build:
|
||||
tailwindcss build -i {{STATIC_DIR}}/css/main.css -o {{STATIC_DIR}}/css/tailwind.css
|
||||
tailwindcss build -c build/tailwind.config.js -i {{STATIC_DIR}}/css/main.css -o {{STATIC_DIR}}/css/tailwind.css
|
||||
|
||||
_htmx-download:
|
||||
mkdir -p {{STATIC_DIR}}/js
|
||||
|
|
|
@ -1,34 +1,48 @@
|
|||
package main
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/handlers"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/temporal"
|
||||
"code.tjo.space/mentos1386/zdravko/web/static"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.NewConfig()
|
||||
type Server struct {
|
||||
server *http.Server
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config) (*Server, error) {
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Name() string {
|
||||
return "HTTP WEB and API Server"
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
r := mux.NewRouter()
|
||||
|
||||
db, query, err := internal.ConnectToDatabase(cfg.DatabasePath)
|
||||
db, query, err := internal.ConnectToDatabase(s.cfg.DatabasePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
log.Println("Connected to database")
|
||||
|
||||
temporalClient, err := internal.ConnectToTemporal(cfg)
|
||||
temporalClient, err := temporal.ConnectToTemporal(s.cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
h := handlers.NewBaseHandler(db, query, temporalClient, cfg)
|
||||
h := handlers.NewBaseHandler(db, query, temporalClient, s.cfg)
|
||||
|
||||
// Health
|
||||
r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -69,6 +83,15 @@ func main() {
|
|||
// 404
|
||||
r.PathPrefix("/").HandlerFunc(h.Error404).Methods("GET")
|
||||
|
||||
log.Println("Server started on", cfg.Port)
|
||||
log.Fatal(http.ListenAndServe(":"+cfg.Port, r))
|
||||
s.server = &http.Server{
|
||||
Addr: ":" + s.cfg.Port,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
ctx := context.Background()
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
55
pkg/temporal/temporal.go
Normal file
55
pkg/temporal/temporal.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package temporal
|
||||
|
||||
import (
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"github.com/temporalio/ui-server/v2/server"
|
||||
t "go.temporal.io/server/temporal"
|
||||
)
|
||||
|
||||
type Temporal struct {
|
||||
server t.Server
|
||||
uiServer *server.Server
|
||||
}
|
||||
|
||||
func NewTemporal(cfg *config.Config) (*Temporal, error) {
|
||||
serverConfig := NewServerConfig(cfg)
|
||||
server, err := NewServer(serverConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uiConfig := NewUiConfig(cfg)
|
||||
uiServer, err := NewUiServer(uiConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Temporal{
|
||||
server: server,
|
||||
uiServer: uiServer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Temporal) Name() string {
|
||||
return "Temporal UI and Server"
|
||||
}
|
||||
|
||||
func (t *Temporal) Start() error {
|
||||
go func() {
|
||||
err := t.uiServer.Start()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
return t.server.Start()
|
||||
}
|
||||
|
||||
func (t *Temporal) Stop() error {
|
||||
err := t.server.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.uiServer.Stop()
|
||||
|
||||
return nil
|
||||
}
|
48
pkg/worker/worker.go
Normal file
48
pkg/worker/worker.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package worker
|
||||
|
||||
import (
|
||||
"code.tjo.space/mentos1386/zdravko/internal/activities"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/config"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/temporal"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/workflows"
|
||||
"go.temporal.io/sdk/worker"
|
||||
)
|
||||
|
||||
type Worker struct {
|
||||
worker worker.Worker
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewWorker(cfg *config.Config) (*Worker, error) {
|
||||
return &Worker{
|
||||
cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Worker) Name() string {
|
||||
return "Temporal Worker"
|
||||
}
|
||||
|
||||
func (w *Worker) Start() error {
|
||||
temporalClient, err := temporal.ConnectToTemporal(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{})
|
||||
|
||||
// Register Workflows
|
||||
w.worker.RegisterWorkflow(workflows.HealthcheckHttpWorkflowDefinition)
|
||||
|
||||
// Register Activities
|
||||
w.worker.RegisterActivity(activities.HealthcheckHttpActivityDefinition)
|
||||
|
||||
return w.worker.Run(worker.InterruptCh())
|
||||
}
|
||||
|
||||
func (w *Worker) Stop() error {
|
||||
w.worker.Stop()
|
||||
return nil
|
||||
}
|
|
@ -5,15 +5,7 @@ processes:
|
|||
command: watchexec -r -e go,tmpl,css just _tailwindcss-build
|
||||
availability:
|
||||
restart: "always"
|
||||
server:
|
||||
command: watchexec -r -e go,tmpl,css just run-server
|
||||
availability:
|
||||
restart: "always"
|
||||
worker:
|
||||
command: watchexec -r -e go,tmpl,css just run-worker
|
||||
availability:
|
||||
restart: "always"
|
||||
temporal:
|
||||
command: just run-temporal
|
||||
zdravko:
|
||||
command: watchexec -r -e go,tmpl,css just run-zdravko
|
||||
availability:
|
||||
restart: "always"
|
||||
|
|
Loading…
Reference in a new issue