feat: single binary

This commit is contained in:
Tine 2024-02-16 22:31:00 +01:00
parent 97b2d86cbf
commit 0db0f3e6b8
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
16 changed files with 400 additions and 142 deletions

3
.dockerignore Normal file
View file

@ -0,0 +1,3 @@
build
deploy
docs

55
.github/workflows/build.yaml vendored Normal file
View 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
View 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"]

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package internal
package temporal
import (
"time"

View file

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

View file

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

View file

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