mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-19 08:44:14 +00:00
fix quota message
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# Build targets for the ATProto Container Registry
|
||||
|
||||
.PHONY: all build build-appview build-hold build-credential-helper build-oauth-helper \
|
||||
build-trixie \
|
||||
generate test test-race test-verbose lint lex-lint clean help install-credential-helper \
|
||||
develop develop-detached develop-down dev \
|
||||
docker docker-appview docker-hold docker-scanner
|
||||
@@ -59,6 +60,34 @@ build-oauth-helper: ## Build OAuth helper only
|
||||
@mkdir -p bin
|
||||
go build -o bin/oauth-helper ./cmd/oauth-helper
|
||||
|
||||
# Trixie cross-build (Debian 13, glibc 2.41) — produces binaries that run on
|
||||
# any glibc ≥ 2.41 target, even when the host glibc is newer (e.g. Fedora's
|
||||
# 2.43, which otherwise stamps sqrtf@GLIBC_2.43 onto cgo-linked output).
|
||||
TRIXIE_BUILDER_IMAGE ?= golang:1-trixie
|
||||
|
||||
build-trixie: $(GENERATED_ASSETS) ## Build all production binaries (appview, hold, credential-helper, scanner, labeler) for linux/amd64 in a Debian 13 (glibc 2.41) container
|
||||
@echo "→ Building in $(TRIXIE_BUILDER_IMAGE) for glibc 2.41 compatibility..."
|
||||
@mkdir -p bin
|
||||
docker run --rm \
|
||||
--user $$(id -u):$$(id -g) \
|
||||
-v "$(CURDIR)":/src \
|
||||
-w /src \
|
||||
-e HOME=/tmp \
|
||||
-e GOCACHE=/tmp/.gocache \
|
||||
-e GOMODCACHE=/tmp/.gomodcache \
|
||||
-e CGO_ENABLED=1 \
|
||||
-e GOOS=linux \
|
||||
-e GOARCH=amd64 \
|
||||
$(TRIXIE_BUILDER_IMAGE) \
|
||||
bash -c '\
|
||||
set -e && \
|
||||
go build -trimpath -ldflags="-s -w $(APPVIEW_LDFLAGS)" -o bin/atcr-appview ./cmd/appview && \
|
||||
go build -trimpath -ldflags="-s -w" -o bin/atcr-hold ./cmd/hold && \
|
||||
go build -trimpath -ldflags="-s -w" -o bin/docker-credential-atcr ./cmd/credential-helper && \
|
||||
go build -trimpath -ldflags="-s -w" -o bin/atcr-labeler ./cmd/labeler && \
|
||||
cd scanner && go build -trimpath -ldflags="-s -w" -o ../bin/atcr-scanner ./cmd/scanner'
|
||||
@echo "✓ Built to bin/ (glibc ≥ 2.41 compatible)"
|
||||
|
||||
##@ Test Targets
|
||||
|
||||
test: ## Run all tests
|
||||
|
||||
@@ -371,30 +371,8 @@ func cmdProvision(token, zone, plan, sshKeyPath, s3Secret string, withScanner, w
|
||||
return fmt.Errorf("go generate: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\nBuilding locally (GOOS=linux GOARCH=amd64)...")
|
||||
if appviewCreated {
|
||||
outputPath := filepath.Join(rootDir, "bin", "atcr-appview")
|
||||
if err := buildLocal(rootDir, outputPath, "./cmd/appview"); err != nil {
|
||||
return fmt.Errorf("build appview: %w", err)
|
||||
}
|
||||
if state.LabelerEnabled {
|
||||
outputPath := filepath.Join(rootDir, "bin", "atcr-labeler")
|
||||
if err := buildLocal(rootDir, outputPath, "./cmd/labeler"); err != nil {
|
||||
return fmt.Errorf("build labeler: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if holdCreated {
|
||||
outputPath := filepath.Join(rootDir, "bin", "atcr-hold")
|
||||
if err := buildLocal(rootDir, outputPath, "./cmd/hold"); err != nil {
|
||||
return fmt.Errorf("build hold: %w", err)
|
||||
}
|
||||
if state.ScannerEnabled {
|
||||
outputPath := filepath.Join(rootDir, "bin", "atcr-scanner")
|
||||
if err := buildLocal(filepath.Join(rootDir, "scanner"), outputPath, "./cmd/scanner"); err != nil {
|
||||
return fmt.Errorf("build scanner: %w", err)
|
||||
}
|
||||
}
|
||||
if err := runMakeBuildTrixie(rootDir); err != nil {
|
||||
return fmt.Errorf("build: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\nWaiting for cloud-init to complete on new servers...")
|
||||
@@ -447,9 +425,8 @@ func cmdProvision(token, zone, plan, sshKeyPath, s3Secret string, withScanner, w
|
||||
if err := runGenerate(rootDir); err != nil {
|
||||
return fmt.Errorf("go generate: %w", err)
|
||||
}
|
||||
fmt.Println("\nBuilding labeler locally (GOOS=linux GOARCH=amd64)...")
|
||||
labelerLocal := filepath.Join(rootDir, "bin", "atcr-labeler")
|
||||
if err := buildLocal(rootDir, labelerLocal, "./cmd/labeler"); err != nil {
|
||||
if err := runMakeBuildTrixie(rootDir); err != nil {
|
||||
return fmt.Errorf("build labeler: %w", err)
|
||||
}
|
||||
labelerRemote := naming.InstallDir() + "/bin/" + naming.Labeler()
|
||||
|
||||
+12
-53
@@ -127,44 +127,10 @@ func cmdUpdate(target string, withScanner, withLabeler bool) error {
|
||||
return fmt.Errorf("go generate: %w", err)
|
||||
}
|
||||
|
||||
// Build all binaries locally before touching servers
|
||||
fmt.Println("Building locally (GOOS=linux GOARCH=amd64)...")
|
||||
for _, name := range toUpdate {
|
||||
t := targets[name]
|
||||
outputPath := filepath.Join(rootDir, "bin", t.localBinary)
|
||||
if err := buildLocal(rootDir, outputPath, "./cmd/"+t.buildCmd); err != nil {
|
||||
return fmt.Errorf("build %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Build scanner locally if needed
|
||||
needScanner := false
|
||||
for _, name := range toUpdate {
|
||||
if name == "hold" && state.ScannerEnabled {
|
||||
needScanner = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if needScanner {
|
||||
outputPath := filepath.Join(rootDir, "bin", "atcr-scanner")
|
||||
if err := buildLocal(filepath.Join(rootDir, "scanner"), outputPath, "./cmd/scanner"); err != nil {
|
||||
return fmt.Errorf("build scanner: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Build labeler locally if needed
|
||||
needLabeler := false
|
||||
for _, name := range toUpdate {
|
||||
if name == "appview" && state.LabelerEnabled {
|
||||
needLabeler = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if needLabeler {
|
||||
outputPath := filepath.Join(rootDir, "bin", "atcr-labeler")
|
||||
if err := buildLocal(rootDir, outputPath, "./cmd/labeler"); err != nil {
|
||||
return fmt.Errorf("build labeler: %w", err)
|
||||
}
|
||||
// Build all binaries via `make build-trixie` so output links against
|
||||
// glibc 2.41 (the deploy target's glibc) regardless of the host's glibc.
|
||||
if err := runMakeBuildTrixie(rootDir); err != nil {
|
||||
return fmt.Errorf("build: %w", err)
|
||||
}
|
||||
|
||||
// Deploy each target
|
||||
@@ -392,21 +358,14 @@ func runGenerate(dir string) error {
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// buildLocal compiles a Go binary locally with cross-compilation flags for linux/amd64.
|
||||
func buildLocal(dir, outputPath, buildPkg string) error {
|
||||
fmt.Printf(" building %s...\n", filepath.Base(outputPath))
|
||||
cmd := exec.Command("go", "build",
|
||||
"-ldflags=-s -w",
|
||||
"-trimpath",
|
||||
"-o", outputPath,
|
||||
buildPkg,
|
||||
)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GOOS=linux",
|
||||
"GOARCH=amd64",
|
||||
"CGO_ENABLED=1",
|
||||
)
|
||||
// runMakeBuildTrixie shells out to `make build-trixie`, which builds all
|
||||
// production binaries (appview, hold, credential-helper, labeler, scanner)
|
||||
// inside a Debian 13 container so they link against glibc 2.41. Centralizing
|
||||
// the build recipe in the Makefile keeps deploy and local builds in sync.
|
||||
func runMakeBuildTrixie(rootDir string) error {
|
||||
fmt.Println("Running `make build-trixie` (linux/amd64, glibc 2.41)...")
|
||||
cmd := exec.Command("make", "build-trixie")
|
||||
cmd.Dir = rootDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
|
||||
@@ -239,12 +239,31 @@ func (a *Authorizer) checkQuota(ctx context.Context, userDID, holdDID string) er
|
||||
}
|
||||
|
||||
if stats.Limit != nil && stats.TotalSize >= *stats.Limit {
|
||||
return fmt.Errorf("quota exceeded: %d / %d bytes used by %s. Delete images to free space",
|
||||
stats.TotalSize, *stats.Limit, userDID)
|
||||
return fmt.Errorf("quota exceeded: %s / %s used by %s. Delete images to free space",
|
||||
formatGB(stats.TotalSize), formatGB(*stats.Limit), a.identityLabel(ctx, userDID))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatGB renders bytes as gigabytes with two decimals — matches the
|
||||
// percentage display in the appview UI so users see consistent units.
|
||||
func formatGB(b int64) string {
|
||||
return fmt.Sprintf("%.2f GB", float64(b)/(1024*1024*1024))
|
||||
}
|
||||
|
||||
// identityLabel returns "handle (did)" when the local users table knows the
|
||||
// handle, falling back to the bare DID. The users table is Jetstream-fed, so
|
||||
// for first-seen identities (rare on a push, since they've already OAuth'd)
|
||||
// the handle may be missing — the DID alone still uniquely identifies them.
|
||||
func (a *Authorizer) identityLabel(ctx context.Context, did string) string {
|
||||
var handle string
|
||||
err := a.db.QueryRowContext(ctx, "SELECT handle FROM users WHERE did = ?", did).Scan(&handle)
|
||||
if err != nil || handle == "" || handle == did {
|
||||
return did
|
||||
}
|
||||
return fmt.Sprintf("%s (%s)", handle, did)
|
||||
}
|
||||
|
||||
// serviceTokenFetcher returns the appropriate fetcher for the auth method,
|
||||
// or nil if none is available (e.g. OAuth flow with no refresher configured).
|
||||
// ctx is used for the cold-cache identity lookup inside resolvePDS.
|
||||
|
||||
@@ -230,21 +230,43 @@ func TestCheckQuota_OverLimit(t *testing.T) {
|
||||
atproto.SetTestMode(true)
|
||||
t.Cleanup(func() { atproto.SetTestMode(false) })
|
||||
|
||||
srv := quotaServer(t, 200, `{"totalSize":1000,"limit":1000}`)
|
||||
a := New(newTestDB(t), fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
||||
// 5 GiB exactly so the formatted message shows "5.00 GB / 5.00 GB".
|
||||
srv := quotaServer(t, 200, `{"totalSize":5368709120,"limit":5368709120}`)
|
||||
d := newTestDB(t)
|
||||
seedUser(t, d, "did:plc:alice", "alice.bsky.social", "")
|
||||
a := New(d, fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
||||
|
||||
err := a.checkQuota(context.Background(), "did:plc:alice", srv.holdDID)
|
||||
if err == nil {
|
||||
t.Fatal("checkQuota at limit should deny")
|
||||
}
|
||||
msg := err.Error()
|
||||
for _, want := range []string{"quota exceeded", "1000", "did:plc:alice"} {
|
||||
for _, want := range []string{"quota exceeded", "5.00 GB", "did:plc:alice", "alice.bsky.social"} {
|
||||
if !strings.Contains(msg, want) {
|
||||
t.Errorf("expected %q in error %q", want, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When no users row exists for the DID (handle unknown), the error still
|
||||
// formats correctly with the bare DID.
|
||||
func TestCheckQuota_OverLimit_NoHandle(t *testing.T) {
|
||||
atproto.SetTestMode(true)
|
||||
t.Cleanup(func() { atproto.SetTestMode(false) })
|
||||
|
||||
srv := quotaServer(t, 200, `{"totalSize":5368709120,"limit":5368709120}`)
|
||||
a := New(newTestDB(t), fakeHoldAuthorizer{}, nil, "", WithHTTPClient(srv.httpClient()))
|
||||
|
||||
err := a.checkQuota(context.Background(), "did:plc:bob", srv.holdDID)
|
||||
if err == nil {
|
||||
t.Fatal("checkQuota at limit should deny")
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "did:plc:bob") || strings.Contains(msg, "(did:") {
|
||||
t.Errorf("expected bare DID (no parenthesized form) in error %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckQuota_NilLimitAllows(t *testing.T) {
|
||||
// A user on the unlimited tier has limit == nil. Even huge totalSize
|
||||
// must not deny.
|
||||
|
||||
Vendored
+17
-4
File diff suppressed because one or more lines are too long
@@ -33,7 +33,9 @@
|
||||
</div>
|
||||
{{/* Color alone on the progress bar doesn't meet AAA — repeat the warning
|
||||
as an alert block once usage crosses the threshold. */}}
|
||||
{{ if ge .UsagePercent 95 }}
|
||||
{{ if ge .UsagePercent 100 }}
|
||||
{{ template "alert" (dict "Type" "error" "Message" "You've exceeded your storage limit. New pushes will be rejected until you delete images to free space.") }}
|
||||
{{ else if ge .UsagePercent 95 }}
|
||||
{{ template "alert" (dict "Type" "error" "Message" "You're nearly at your storage limit. Pushes may fail once you exceed it.") }}
|
||||
{{ else if ge .UsagePercent 80 }}
|
||||
{{ template "alert" (dict "Type" "warning" "Message" "You're using most of your storage quota. Consider cleaning up untagged images.") }}
|
||||
|
||||
+14
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user