Harden CI and switch release builds to tagged immutable images
This commit is contained in:
parent
bed2e6cfb6
commit
f24e96efa7
60 changed files with 4710 additions and 64 deletions
2
internal/adapters/postgres/doc.go
Normal file
2
internal/adapters/postgres/doc.go
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Package postgres contains PostgreSQL repository implementations generated from sqlc queries.
|
||||
package postgres
|
||||
2
internal/adapters/storage/doc.go
Normal file
2
internal/adapters/storage/doc.go
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Package storage is the only package permitted to access managed storage paths.
|
||||
package storage
|
||||
32
internal/adapters/webassets/handler.go
Normal file
32
internal/adapters/webassets/handler.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Package webassets serves the compiled frontend without giving transport code filesystem access.
|
||||
package webassets
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func New(root string) http.Handler {
|
||||
files := os.DirFS(root)
|
||||
fileServer := http.FileServer(http.FS(files))
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requested := strings.TrimPrefix(filepath.Clean(r.URL.Path), string(filepath.Separator))
|
||||
if requested == "." {
|
||||
requested = "index.html"
|
||||
}
|
||||
if _, err := fs.Stat(files, requested); err == nil {
|
||||
fileServer.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if _, err := fs.Stat(files, "index.html"); err == nil {
|
||||
r.URL.Path = "/"
|
||||
fileServer.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Drive web assets are not built", http.StatusServiceUnavailable)
|
||||
})
|
||||
}
|
||||
2
internal/application/doc.go
Normal file
2
internal/application/doc.go
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Package application coordinates transactional Drive use cases through ports.
|
||||
package application
|
||||
77
internal/architecture/architecture_test.go
Normal file
77
internal/architecture/architecture_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package architecture_test
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestImportBoundaries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
repositoryRoot := filepath.Join("..", "..")
|
||||
rules := []struct {
|
||||
directory string
|
||||
forbidden []string
|
||||
}{
|
||||
{
|
||||
directory: "internal/domain",
|
||||
forbidden: []string{"net/http", "database/sql", "github.com/jackc/pgx", "/internal/adapters/", "/internal/transport/"},
|
||||
},
|
||||
{
|
||||
directory: "internal/transport",
|
||||
forbidden: []string{"database/sql", "github.com/jackc/pgx", "os"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
rule := rule
|
||||
t.Run(rule.directory, func(t *testing.T) {
|
||||
checkDirectory(t, repositoryRoot, rule.directory, rule.forbidden)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkDirectory(t *testing.T, root, directory string, forbidden []string) {
|
||||
t.Helper()
|
||||
|
||||
err := filepath.WalkDir(filepath.Join(root, directory), func(path string, entry os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, spec := range parsed.Imports {
|
||||
assertImportAllowed(t, path, spec, forbidden)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk %s: %v", directory, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertImportAllowed(t *testing.T, path string, spec *ast.ImportSpec, forbidden []string) {
|
||||
t.Helper()
|
||||
|
||||
importPath, err := strconv.Unquote(spec.Path.Value)
|
||||
if err != nil {
|
||||
t.Fatalf("unquote import in %s: %v", path, err)
|
||||
}
|
||||
for _, pattern := range forbidden {
|
||||
if importPath == pattern || strings.Contains(importPath, pattern) {
|
||||
t.Errorf("%s imports forbidden package %q", path, importPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
internal/config/config.go
Normal file
55
internal/config/config.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
HTTPAddress string
|
||||
WebRoot string
|
||||
StorageRoot string
|
||||
PreviewCacheDir string
|
||||
DatabaseURL string
|
||||
LogLevel slog.Level
|
||||
}
|
||||
|
||||
func FromEnvironment() (Config, error) {
|
||||
level, err := parseLogLevel(environment("DRIVE_LOG_LEVEL", "info"))
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
return Config{
|
||||
HTTPAddress: environment("DRIVE_HTTP_ADDRESS", ":8080"),
|
||||
WebRoot: environment("DRIVE_WEB_ROOT", "web/dist"),
|
||||
StorageRoot: environment("DRIVE_STORAGE_ROOT", "/storage/.drive"),
|
||||
PreviewCacheDir: environment("DRIVE_PREVIEW_CACHE_ROOT", "/cache/previews"),
|
||||
DatabaseURL: os.Getenv("DRIVE_DATABASE_URL"),
|
||||
LogLevel: level,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func environment(name, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func parseLogLevel(value string) (slog.Level, error) {
|
||||
switch strings.ToLower(value) {
|
||||
case "debug":
|
||||
return slog.LevelDebug, nil
|
||||
case "info":
|
||||
return slog.LevelInfo, nil
|
||||
case "warn", "warning":
|
||||
return slog.LevelWarn, nil
|
||||
case "error":
|
||||
return slog.LevelError, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid DRIVE_LOG_LEVEL %q", value)
|
||||
}
|
||||
}
|
||||
18
internal/config/config_test.go
Normal file
18
internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseLogLevel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
level, err := parseLogLevel("warning")
|
||||
if err != nil {
|
||||
t.Fatalf("parseLogLevel returned an error: %v", err)
|
||||
}
|
||||
if level != slog.LevelWarn {
|
||||
t.Fatalf("parseLogLevel = %v, want %v", level, slog.LevelWarn)
|
||||
}
|
||||
}
|
||||
4
internal/domain/doc.go
Normal file
4
internal/domain/doc.go
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Package domain contains Drive's business rules and value types.
|
||||
//
|
||||
// It must not import HTTP, SQL, PostgreSQL, or filesystem adapter packages.
|
||||
package domain
|
||||
63
internal/transport/httpapi/server.go
Normal file
63
internal/transport/httpapi/server.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"drive.local/drivev2/internal/config"
|
||||
)
|
||||
|
||||
var ErrServerClosed = http.ErrServerClosed
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
}
|
||||
|
||||
func NewServer(cfg config.Config, version string, logger *slog.Logger, frontend http.Handler) *Server {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /health/live", jsonResponse(http.StatusOK, map[string]string{"status": "live"}))
|
||||
mux.HandleFunc("GET /health/ready", jsonResponse(http.StatusOK, map[string]string{"status": "ready"}))
|
||||
mux.HandleFunc("GET /api/v1/setup/status", jsonResponse(http.StatusOK, map[string]any{
|
||||
"initialized": false,
|
||||
"phase": "foundation",
|
||||
"version": version,
|
||||
}))
|
||||
mux.Handle("/", frontend)
|
||||
|
||||
return &Server{httpServer: &http.Server{
|
||||
Addr: cfg.HTTPAddress,
|
||||
Handler: requestLogger(logger, mux),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
IdleTimeout: 2 * time.Minute,
|
||||
}}
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe() error {
|
||||
return s.httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
return s.httpServer.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func jsonResponse(status int, payload any) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
slog.Error("encode response", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requestLogger(logger *slog.Logger, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
started := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
logger.Info("http request", "method", r.Method, "path", r.URL.Path, "duration_ms", time.Since(started).Milliseconds())
|
||||
})
|
||||
}
|
||||
34
internal/transport/httpapi/server_test.go
Normal file
34
internal/transport/httpapi/server_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"drive.local/drivev2/internal/config"
|
||||
)
|
||||
|
||||
func TestLiveHealth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := NewServer(
|
||||
config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()},
|
||||
"test",
|
||||
slog.New(slog.NewTextHandler(io.Discard, nil)),
|
||||
http.NotFoundHandler(),
|
||||
)
|
||||
request := httptest.NewRequest(http.MethodGet, "/health/live", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
server.httpServer.Handler.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
|
||||
}
|
||||
if !strings.Contains(recorder.Body.String(), `"status":"live"`) {
|
||||
t.Fatalf("unexpected body: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
2
internal/workers/doc.go
Normal file
2
internal/workers/doc.go
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Package workers runs durable PostgreSQL-backed jobs.
|
||||
package workers
|
||||
Loading…
Add table
Add a link
Reference in a new issue