Add production Docker deployment
Amp-Thread-ID: https://ampcode.com/threads/T-01a02485-5574-707c-bff4-5668d83bee8a Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -53,6 +53,7 @@ func newApplication(store *playgroundStore, packages map[string]memoryAsset, mon
|
||||
|
||||
func (app *application) routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", app.health)
|
||||
mux.HandleFunc("/pkg/", app.servePackage)
|
||||
mux.HandleFunc("/api/playgrounds", app.savePlayground)
|
||||
mux.HandleFunc("/api/playgrounds/", app.getPlayground)
|
||||
@@ -66,6 +67,15 @@ func (app *application) routes() http.Handler {
|
||||
return app.headers(mux)
|
||||
}
|
||||
|
||||
func (app *application) health(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method != http.MethodGet && request.Method != http.MethodHead {
|
||||
response.Header().Set("Allow", "GET, HEAD")
|
||||
http.Error(response, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
response.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (app *application) headers(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
response.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
|
||||
|
||||
@@ -57,6 +57,15 @@ func TestPackageIsServedFromMemory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealth(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
response := httptest.NewRecorder()
|
||||
testApplication(t).routes().ServeHTTP(response, request)
|
||||
if response.Code != http.StatusNoContent {
|
||||
t.Fatalf("status = %d", response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveAndLoadPlaygroundRevision(t *testing.T) {
|
||||
app := testApplication(t)
|
||||
handler := app.routes()
|
||||
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repository="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
expected_repository="$HOME/yawn"
|
||||
|
||||
if [[ "$repository" != "$expected_repository" ]]; then
|
||||
printf 'Expected the repository at %s, found %s\n' "$expected_repository" "$repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for command in git docker; do
|
||||
if ! command -v "$command" >/dev/null 2>&1; then
|
||||
printf 'Missing required command: %s\n' "$command" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
printf 'Docker Compose v2 is required.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$repository"
|
||||
|
||||
if [[ "$(git branch --show-current)" != "main" ]]; then
|
||||
printf 'Deployments must run from the main branch.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then
|
||||
printf 'Refusing to deploy a checkout with local changes.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Updating main...\n'
|
||||
git fetch origin main
|
||||
fetched="$(git rev-parse FETCH_HEAD)"
|
||||
git merge --ff-only "$fetched"
|
||||
if [[ "$(git rev-parse HEAD)" != "$fetched" ]]; then
|
||||
printf 'HEAD does not match origin/main after updating.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Building the production image...\n'
|
||||
docker compose build --pull yawn
|
||||
docker compose up -d --remove-orphans yawn
|
||||
|
||||
container="$(docker compose ps -q yawn)"
|
||||
if [[ -z "$container" ]]; then
|
||||
printf 'Docker Compose did not create the Yawn container.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Waiting for Yawn to become healthy...\n'
|
||||
for _ in {1..60}; do
|
||||
status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container")"
|
||||
case "$status" in
|
||||
healthy)
|
||||
printf 'The Yawn container is healthy.\n'
|
||||
exit 0
|
||||
;;
|
||||
unhealthy | exited | dead)
|
||||
docker compose logs --tail 50 yawn >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
sleep 2
|
||||
done
|
||||
|
||||
printf 'Yawn did not become healthy before the timeout.\n' >&2
|
||||
docker compose logs --tail 50 yawn >&2
|
||||
exit 1
|
||||
+24
-3
@@ -25,10 +25,17 @@ func run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Print("building core and handles packages")
|
||||
packages, err := buildPackages(root)
|
||||
packageDirectory := os.Getenv("YAWN_PACKAGES")
|
||||
var packages map[string]memoryAsset
|
||||
if packageDirectory == "" {
|
||||
log.Print("building core and handles packages")
|
||||
packages, err = buildPackages(root)
|
||||
} else {
|
||||
log.Printf("loading prebuilt packages from %s", packageDirectory)
|
||||
packages, err = loadPackages(packageDirectory)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("build packages: %w", err)
|
||||
return fmt.Errorf("prepare packages: %w", err)
|
||||
}
|
||||
log.Printf("loaded %d package assets into memory", len(packages))
|
||||
|
||||
@@ -84,6 +91,20 @@ func run() error {
|
||||
}
|
||||
|
||||
func repositoryRoot() (string, error) {
|
||||
if configured := os.Getenv("YAWN_ROOT"); configured != "" {
|
||||
root, err := filepath.Abs(configured)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
info, err := os.Stat(root)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return "", fmt.Errorf("YAWN_ROOT is not a directory: %s", root)
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
directory, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
+6
-3
@@ -33,7 +33,10 @@ func buildPackages(root string) (map[string]memoryAsset, error) {
|
||||
if err := runBuild(root, "node", filepath.Join(root, "server", "build.mjs"), root, output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loadPackages(output)
|
||||
}
|
||||
|
||||
func loadPackages(directory string) (map[string]memoryAsset, error) {
|
||||
assets := make(map[string]memoryAsset)
|
||||
for _, name := range []string{
|
||||
"core.js",
|
||||
@@ -42,21 +45,21 @@ func buildPackages(root string) (map[string]memoryAsset, error) {
|
||||
"importer-worker.js",
|
||||
"bvh-worker.js",
|
||||
} {
|
||||
asset, err := loadMemoryAsset(filepath.Join(output, name))
|
||||
asset, err := loadMemoryAsset(filepath.Join(directory, name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assets[name] = asset
|
||||
}
|
||||
|
||||
err = filepath.WalkDir(filepath.Join(output, "pkg"), func(path string, entry fs.DirEntry, walkErr error) error {
|
||||
err := filepath.WalkDir(filepath.Join(directory, "pkg"), func(path string, entry fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
relative, err := filepath.Rel(filepath.Join(output, "pkg"), path)
|
||||
relative, err := filepath.Rel(filepath.Join(directory, "pkg"), path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadPackages(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
for _, name := range []string{
|
||||
"core.js",
|
||||
"handles.js",
|
||||
"worker.js",
|
||||
"importer-worker.js",
|
||||
"bvh-worker.js",
|
||||
filepath.Join("pkg", "yawn_core_bg.wasm"),
|
||||
} {
|
||||
path := filepath.Join(directory, name)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(name), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
packages, err := loadPackages(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(packages) != 6 {
|
||||
t.Fatalf("package count = %d", len(packages))
|
||||
}
|
||||
if content := string(packages["pkg/yawn_core_bg.wasm"].content); content != filepath.Join("pkg", "yawn_core_bg.wasm") {
|
||||
t.Fatalf("WASM content = %q", content)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user