feat: add production build and health tooling
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
bin
|
||||||
|
data
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
FROM golang:1.24-alpine AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
ARG VERSION=dev
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" -o /out/chfs-ng ./cmd/chfs
|
||||||
|
|
||||||
|
FROM alpine:3.22
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
COPY --from=build /out/chfs-ng /usr/local/bin/chfs-ng
|
||||||
|
RUN mkdir -p /data && chmod 755 /data
|
||||||
|
VOLUME ["/data"]
|
||||||
|
EXPOSE 80
|
||||||
|
ENTRYPOINT ["/usr/local/bin/chfs-ng"]
|
||||||
|
CMD ["-path", "/data", "-port", "80"]
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD wget -q -O - http://127.0.0.1/healthz >/dev/null || exit 1
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
APP := chfs-ng
|
||||||
|
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||||
|
LDFLAGS := -s -w -X main.version=$(VERSION)
|
||||||
|
|
||||||
|
.PHONY: build test race vet check run docker-build docker-run
|
||||||
|
|
||||||
|
build:
|
||||||
|
CGO_ENABLED=0 go build -trimpath -ldflags '$(LDFLAGS)' -o bin/$(APP) ./cmd/chfs
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
race:
|
||||||
|
go test -race ./...
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
check: test race vet
|
||||||
|
git diff --check
|
||||||
|
|
||||||
|
run:
|
||||||
|
go run -ldflags '$(LDFLAGS)' ./cmd/chfs
|
||||||
|
|
||||||
|
docker-build:
|
||||||
|
docker build --build-arg VERSION=$(VERSION) -t $(APP):$(VERSION) -t $(APP):latest .
|
||||||
|
|
||||||
|
docker-run:
|
||||||
|
docker run --rm -p 8080:80 -v $(PWD)/data:/data $(APP):latest -path /data -port 80
|
||||||
@@ -16,7 +16,10 @@ import (
|
|||||||
"chfs-ng/internal/storage"
|
"chfs-ng/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
showVersion := flag.Bool("version", false, "print version and exit")
|
||||||
file := flag.String("file", "", "configuration file")
|
file := flag.String("file", "", "configuration file")
|
||||||
pathFlag := flag.String("path", "/data", "shared path")
|
pathFlag := flag.String("path", "/data", "shared path")
|
||||||
port := flag.Int("port", 80, "listen port")
|
port := flag.Int("port", 80, "listen port")
|
||||||
@@ -24,6 +27,11 @@ func main() {
|
|||||||
var rules multiFlag
|
var rules multiFlag
|
||||||
flag.Var(&rules, "rule", "access rule (repeatable)")
|
flag.Var(&rules, "rule", "access rule (repeatable)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
if *showVersion {
|
||||||
|
println(version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api.Version = version
|
||||||
|
|
||||||
cfg := config.Default()
|
cfg := config.Default()
|
||||||
if *file != "" {
|
if *file != "" {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
chfs:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
args:
|
||||||
|
VERSION: ${VERSION:-dev}
|
||||||
|
image: chfs-ng:${VERSION:-latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
command: ["-path", "/data", "-port", "80"]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type healthResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
|
func (s *Server) health(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
|
w.Header().Set("Allow", "GET, HEAD")
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
_ = json.NewEncoder(w).Encode(healthResponse{Status: "ok", Version: Version})
|
||||||
|
}
|
||||||
@@ -33,3 +33,23 @@ func TestRootEscapesPlainNotice(t *testing.T) {
|
|||||||
t.Fatal("plain notice was not escaped")
|
t.Fatal("plain notice was not escaped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHealthz(t *testing.T) {
|
||||||
|
s := &Server{}
|
||||||
|
old := Version
|
||||||
|
Version = "test"
|
||||||
|
defer func() { Version = old }()
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||||
|
s.Handler().ServeHTTP(rr, req)
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status=%d body=%s", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
if got := rr.Header().Get("Cache-Control"); got != "no-store" {
|
||||||
|
t.Fatalf("cache-control=%q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rr.Body.String(), `"status":"ok"`) || !strings.Contains(rr.Body.String(), `"version":"test"`) {
|
||||||
|
t.Fatalf("unexpected body=%s", rr.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type fileInfo struct {
|
|||||||
|
|
||||||
func (s *Server) Handler() http.Handler {
|
func (s *Server) Handler() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/healthz", s.health)
|
||||||
mux.HandleFunc("/webdav/", s.webdav)
|
mux.HandleFunc("/webdav/", s.webdav)
|
||||||
mux.HandleFunc("/webdav", s.webdav)
|
mux.HandleFunc("/webdav", s.webdav)
|
||||||
mux.HandleFunc("/chfs/files", s.files)
|
mux.HandleFunc("/chfs/files", s.files)
|
||||||
|
|||||||
Reference in New Issue
Block a user