Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
157bfbdd33 | ||
|
|
3a2eb1f4d2 |
+5
-1
@@ -1,4 +1,8 @@
|
|||||||
.git
|
# .git is sent without its config. Without a VERSION build argument the
|
||||||
|
# stage that compiles runs `git describe --tags --always` on .git, which
|
||||||
|
# does not need .git/config; that file can hold a credential, such as a
|
||||||
|
# password in a remote URL or the token the CI checkout step stores there.
|
||||||
|
.git/config
|
||||||
.gitignore
|
.gitignore
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env*
|
.env*
|
||||||
|
|||||||
+2
-10
@@ -114,13 +114,11 @@ import (
|
|||||||
var (
|
var (
|
||||||
Appname string = "CHANGEME"
|
Appname string = "CHANGEME"
|
||||||
Version string
|
Version string
|
||||||
Buildarch string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
globals.Appname = Appname
|
globals.Appname = Appname
|
||||||
globals.Version = Version
|
globals.Version = Version
|
||||||
globals.Buildarch = Buildarch
|
|
||||||
|
|
||||||
fx.New(
|
fx.New(
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
@@ -859,7 +857,7 @@ func (l *Logger) Identify() {
|
|||||||
l.log.Info("starting",
|
l.log.Info("starting",
|
||||||
"appname", l.params.Globals.Appname,
|
"appname", l.params.Globals.Appname,
|
||||||
"version", l.params.Globals.Version,
|
"version", l.params.Globals.Version,
|
||||||
"buildarch", l.params.Globals.Buildarch,
|
"arch", runtime.GOARCH,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -981,20 +979,17 @@ import "go.uber.org/fx"
|
|||||||
var (
|
var (
|
||||||
Appname string
|
Appname string
|
||||||
Version string
|
Version string
|
||||||
Buildarch string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Struct for DI
|
// Struct for DI
|
||||||
type Globals struct {
|
type Globals struct {
|
||||||
Appname string
|
Appname string
|
||||||
Version string
|
Version string
|
||||||
Buildarch string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(lc fx.Lifecycle) (*Globals, error) {
|
func New(lc fx.Lifecycle) (*Globals, error) {
|
||||||
n := &Globals{
|
n := &Globals{
|
||||||
Appname: Appname,
|
Appname: Appname,
|
||||||
Buildarch: Buildarch,
|
|
||||||
Version: Version,
|
Version: Version,
|
||||||
}
|
}
|
||||||
return n, nil
|
return n, nil
|
||||||
@@ -1008,13 +1003,11 @@ func New(lc fx.Lifecycle) (*Globals, error) {
|
|||||||
var (
|
var (
|
||||||
Appname string = "CHANGEME" // Default, overridden by build
|
Appname string = "CHANGEME" // Default, overridden by build
|
||||||
Version string // Set at build time
|
Version string // Set at build time
|
||||||
Buildarch string // Set at build time
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
globals.Appname = Appname
|
globals.Appname = Appname
|
||||||
globals.Version = Version
|
globals.Version = Version
|
||||||
globals.Buildarch = Buildarch
|
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -1025,10 +1018,9 @@ Use ldflags to inject version information at build time:
|
|||||||
|
|
||||||
```makefile
|
```makefile
|
||||||
VERSION := $(shell git describe --tags --always)
|
VERSION := $(shell git describe --tags --always)
|
||||||
BUILDARCH := $(shell go env GOARCH)
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -ldflags "-X main.Version=$(VERSION) -X main.Buildarch=$(BUILDARCH)" ./cmd/httpd
|
go build -ldflags "-X main.Version=$(VERSION)" ./cmd/httpd
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+17
-6
@@ -43,12 +43,23 @@ COPY . .
|
|||||||
RUN make test
|
RUN make test
|
||||||
|
|
||||||
# VERSION is declared here, not earlier: a new value reruns only the
|
# VERSION is declared here, not earlier: a new value reruns only the
|
||||||
# build, not script/bootstrap or the tests. CGO stays enabled for
|
# build, not script/bootstrap or the tests. Given none, the version is
|
||||||
# govips; -trimpath keeps build paths out of the binary, and -s -w
|
# `git describe --tags --always` of the .git in the build context (git
|
||||||
# leave out the symbol table and debug information.
|
# comes from script/bootstrap): the tag on a tagged commit, tag-N-gHASH
|
||||||
ARG VERSION=dev
|
# after one, the short commit when no tag is reachable. A context that
|
||||||
RUN CGO_ENABLED=1 GOTOOLCHAIN=auto go build -trimpath \
|
# carries .git and still yields no version fails the build; one without
|
||||||
-ldflags "-s -w -X main.Version=${VERSION}" \
|
# .git, as from a source tarball, stamps an empty version. CGO stays
|
||||||
|
# enabled for govips; -trimpath keeps build paths out of the binary, and
|
||||||
|
# -s -w leave out the symbol table and debug information.
|
||||||
|
ARG VERSION
|
||||||
|
RUN version="${VERSION:-$(git describe --tags --always)}"; \
|
||||||
|
if [ -e .git ] && { [ -z "$version" ] || [ "$version" = dev ] || \
|
||||||
|
[ "$version" = unknown ]; }; then \
|
||||||
|
echo "the build context carries .git but yields no version" >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
CGO_ENABLED=1 GOTOOLCHAIN=auto go build -trimpath \
|
||||||
|
-ldflags "-s -w -X main.Version=${version}" \
|
||||||
-o /pixad ./cmd/pixad
|
-o /pixad ./cmd/pixad
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ P2: security: referer blacklist
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-02 a plain `docker build .` stamps the tag or short commit, not
|
||||||
|
`dev` (closes #166): `.dockerignore` lets `.git` into the build context,
|
||||||
|
without `.git/config`; with no `VERSION` build argument the `Dockerfile`
|
||||||
|
takes the version from `git describe --tags --always`, and fails the build if
|
||||||
|
the context carries `.git` and no version comes out; `ARG VERSION` has no
|
||||||
|
default; pixad logs its version, with its name and architecture, as its first
|
||||||
|
log line at startup.
|
||||||
- 2026-09-29 the container makes `/var/lib/pixa` usable by itself (closes
|
- 2026-09-29 the container makes `/var/lib/pixa` usable by itself (closes
|
||||||
#159): `deploy/docker-entrypoint.sh` creates the directory if it is missing,
|
#159): `deploy/docker-entrypoint.sh` creates the directory if it is missing,
|
||||||
gives the directory and everything in it to `pixad` when the directory or one
|
gives the directory and everything in it to `pixad` when the directory or one
|
||||||
|
|||||||
+4
-1
@@ -56,6 +56,9 @@ func run(_ *cobra.Command, _ []string) {
|
|||||||
middleware.New,
|
middleware.New,
|
||||||
healthcheck.New,
|
healthcheck.New,
|
||||||
),
|
),
|
||||||
fx.Invoke(func(*server.Server) {}),
|
fx.Invoke(
|
||||||
|
func(log *logger.Logger) { log.Identify() },
|
||||||
|
func(*server.Server) {},
|
||||||
|
),
|
||||||
).Run()
|
).Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.uber.org/fx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// errTestHook is the error returned by the test hooks that fail.
|
|
||||||
var errTestHook = errors.New("test hook failed")
|
|
||||||
|
|
||||||
// TestRunAppExitCode checks the exit code runApp returns: the one a
|
|
||||||
// shutdown request carries, 0 for a request without one (as for SIGINT or
|
|
||||||
// SIGTERM), and 1 when the app fails to start or to stop.
|
|
||||||
func TestRunAppExitCode(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
hook func(shutdowner fx.Shutdowner) fx.Hook
|
|
||||||
want int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "shutdown requested with exit code 1",
|
|
||||||
hook: func(shutdowner fx.Shutdowner) fx.Hook {
|
|
||||||
return fx.StartHook(func() error {
|
|
||||||
return shutdowner.Shutdown(fx.ExitCode(1))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
want: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "shutdown requested without an exit code",
|
|
||||||
hook: func(shutdowner fx.Shutdowner) fx.Hook {
|
|
||||||
return fx.StartHook(func() error {
|
|
||||||
return shutdowner.Shutdown()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
want: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "start fails",
|
|
||||||
hook: func(fx.Shutdowner) fx.Hook {
|
|
||||||
return fx.StartHook(func() error { return errTestHook })
|
|
||||||
},
|
|
||||||
want: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "stop fails",
|
|
||||||
hook: func(shutdowner fx.Shutdowner) fx.Hook {
|
|
||||||
return fx.StartStopHook(
|
|
||||||
func() error { return shutdowner.Shutdown() },
|
|
||||||
func() error { return errTestHook },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
want: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
app := fx.New(
|
|
||||||
fx.NopLogger,
|
|
||||||
fx.Invoke(func(lc fx.Lifecycle, shutdowner fx.Shutdowner) {
|
|
||||||
lc.Append(tc.hook(shutdowner))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
got := runApp(app)
|
|
||||||
t.Logf("runApp() = %d", got)
|
|
||||||
|
|
||||||
if got != tc.want {
|
|
||||||
t.Errorf("runApp() = %d, want %d", got, tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -300,69 +300,3 @@ func TestProcessReleasesSlotOnError(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWaitForProcessing holds a processing slot with a Process call that
|
|
||||||
// cannot finish reading its input. WaitForProcessing must report that image
|
|
||||||
// when its context ends first, wait for it otherwise, return 0 once it has
|
|
||||||
// finished, and give back the slots it took while waiting.
|
|
||||||
func TestWaitForProcessing(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
proc := New(Params{MaxConcurrentProcessing: 2})
|
|
||||||
|
|
||||||
gate := make(chan struct{})
|
|
||||||
entered := make(chan struct{}, 1)
|
|
||||||
results := make(chan error, 1)
|
|
||||||
|
|
||||||
openGate := sync.OnceFunc(func() { close(gate) })
|
|
||||||
t.Cleanup(openGate)
|
|
||||||
|
|
||||||
processInBackground(proc, &gatedReader{
|
|
||||||
data: bytes.NewReader(createTestJPEG(t, 10, 10)), gate: gate,
|
|
||||||
entered: entered, counter: &readingCounter{},
|
|
||||||
}, results)
|
|
||||||
waitForEntries(t, entered, 1)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
stillProcessing := proc.WaitForProcessing(ctx)
|
|
||||||
t.Logf("WaitForProcessing() after its context ended: %d", stillProcessing)
|
|
||||||
|
|
||||||
if stillProcessing != 1 {
|
|
||||||
t.Errorf("WaitForProcessing() after its context ended = %d, want 1",
|
|
||||||
stillProcessing)
|
|
||||||
}
|
|
||||||
|
|
||||||
waited := make(chan int, 1)
|
|
||||||
|
|
||||||
go func() { waited <- proc.WaitForProcessing(t.Context()) }()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case got := <-waited:
|
|
||||||
t.Fatalf("WaitForProcessing() = %d while an image was being processed",
|
|
||||||
got)
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
}
|
|
||||||
|
|
||||||
openGate()
|
|
||||||
|
|
||||||
err := <-results
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Process() error = %v, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case got := <-waited:
|
|
||||||
if got != 0 {
|
|
||||||
t.Errorf("WaitForProcessing() once processing finished = %d, want 0",
|
|
||||||
got)
|
|
||||||
}
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Fatal("WaitForProcessing() did not return once processing finished")
|
|
||||||
}
|
|
||||||
|
|
||||||
if held := len(proc.processingSemaphore); held != 0 {
|
|
||||||
t.Errorf("%d slots still held after WaitForProcessing() returned", held)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/fx"
|
|
||||||
"go.uber.org/fx/fxtest"
|
|
||||||
|
|
||||||
"sneak.berlin/go/pixa/internal/config"
|
|
||||||
"sneak.berlin/go/pixa/internal/globals"
|
|
||||||
"sneak.berlin/go/pixa/internal/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// shutdownRecorder is an fx.Shutdowner that sends the options of each
|
|
||||||
// shutdown request on requests.
|
|
||||||
type shutdownRecorder struct {
|
|
||||||
requests chan []fx.ShutdownOption
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r shutdownRecorder) Shutdown(opts ...fx.ShutdownOption) error {
|
|
||||||
r.requests <- opts
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSentryInitFailureFailsStartup checks that a Sentry DSN that cannot be
|
|
||||||
// used makes the server's start hook fail, so fx stops what has already
|
|
||||||
// started, instead of the process exiting from a goroutine.
|
|
||||||
func TestSentryInitFailureFailsStartup(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
lc := fxtest.NewLifecycle(t)
|
|
||||||
|
|
||||||
log, err := logger.New(lc, logger.Params{Globals: &globals.Globals{}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("logger.New() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = New(lc, Params{
|
|
||||||
Logger: log,
|
|
||||||
Globals: &globals.Globals{Appname: "pixad"},
|
|
||||||
Config: &config.Config{SentryDSN: "not-a-dsn"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("New() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = lc.Start(t.Context())
|
|
||||||
t.Logf("Start() error = %v", err)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Start() error = nil, want the Sentry initialization error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestListenErrorRequestsShutdownWithExitCode1 occupies the server's port
|
|
||||||
// and checks that the listen error asks fx to shut down with exit code 1.
|
|
||||||
func TestListenErrorRequestsShutdownWithExitCode1(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
busy, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", ":0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Listen() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Cleanup(func() { _ = busy.Close() })
|
|
||||||
|
|
||||||
addr, ok := busy.Addr().(*net.TCPAddr)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("listener address %v is not a TCP address", busy.Addr())
|
|
||||||
}
|
|
||||||
|
|
||||||
requests := make(chan []fx.ShutdownOption, 1)
|
|
||||||
s := &Server{
|
|
||||||
log: slog.New(slog.DiscardHandler),
|
|
||||||
config: &config.Config{Port: addr.Port},
|
|
||||||
shutdowner: shutdownRecorder{requests: requests},
|
|
||||||
}
|
|
||||||
s.httpServer = s.newHTTPServer()
|
|
||||||
|
|
||||||
go s.serveUntilShutdown()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case opts := <-requests:
|
|
||||||
t.Logf("shutdown options = %v", opts)
|
|
||||||
|
|
||||||
if len(opts) != 1 || opts[0] != fx.ExitCode(1) {
|
|
||||||
t.Errorf("shutdown options = %v, want [fx.ExitCode(1)]", opts)
|
|
||||||
}
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Fatal("no shutdown was requested after the listen error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user