Files
at-container-registry/pkg/hold/config_test.go
Evan Jarrett de02e1f046 remove distribution from hold, add vulnerability scanning in appview.
1. Removing distribution/distribution from the Hold Service (biggest change)
  The hold service previously used distribution's StorageDriver interface for all blob operations. This replaces it with direct AWS SDK v2 calls through ATCR's own pkg/s3.S3Service:
  - New S3Service methods: Stat(), PutBytes(), Move(), Delete(), WalkBlobs(), ListPrefix() added to pkg/s3/types.go
  - Pull zone fix: Presigned URLs are now generated against the real S3 endpoint, then the host is swapped to the CDN URL post-signing (previously the CDN URL was set as the endpoint, which
  broke SigV4 signatures)
  - All hold subsystems migrated: GC, OCI uploads, XRPC handlers, profile uploads, scan broadcaster, manifest posts — all now use *s3.S3Service instead of storagedriver.StorageDriver
  - Config simplified: Removed configuration.Storage type and buildStorageConfigFromFields(); replaced with a simple S3Params() method
  - Mock expanded: MockS3Client gains an in-memory object store + 5 new methods, replacing duplicate mockStorageDriver implementations in tests (~160 lines deleted from each test file)
2. Vulnerability Scan UI in AppView (new feature)
  Displays scan results from the hold's PDS on the repository page:
  - New lexicon: io/atcr/hold/scan.json with vulnReportBlob field for storing full Grype reports
  - Two new HTMX endpoints: /api/scan-result (badge) and /api/vuln-details (modal with CVE table)
  - New templates: vuln-badge.html (severity count chips) and vuln-details.html (full CVE table with NVD/GHSA links)
  - Repository page: Lazy-loads scan badges per manifest via HTMX
  - Tests: ~590 lines of test coverage for both handlers
3. S3 Diagnostic Tool
  New cmd/s3-test/main.go (418 lines) — tests S3 connectivity with both SDK v1 and v2, including presigned URL generation, pull zone host swapping, and verbose signing debug output.
4. Deployment Tooling
  - New syncServiceUnit() for comparing/updating systemd units on servers
  - Update command now syncs config keys (adds missing keys from template) and service units with daemon-reload
5. DB Migration
  0011_fix_captain_successor_column.yaml — rebuilds hold_captain_records to add the successor column that was missed in a previous migration.
6. Documentation
  - APPVIEW-UI-FUTURE.md rewritten as a status-tracked feature inventory
  - DISTRIBUTION.md renamed to CREDENTIAL_HELPER.md
  - New REMOVING_DISTRIBUTION.md — 480-line analysis of fully removing distribution from the appview side
7. go.mod
  aws-sdk-go v1 moved from indirect to direct (needed by cmd/s3-test).
2026-02-13 15:26:24 -06:00

292 lines
8.2 KiB
Go

package hold
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func init() {
// Point metadata endpoint to a closed listener so it fails instantly instead of
// waiting 2s for the real 169.254.169.254 to timeout on non-cloud machines.
metadataEndpoint = "http://127.0.0.1:1"
}
// setupEnv sets environment variables for testing and returns a cleanup function
func setupEnv(t *testing.T, vars map[string]string) func() {
// Save original env
original := make(map[string]string)
for k := range vars {
original[k] = os.Getenv(k)
}
// Set test env vars
for k, v := range vars {
if err := os.Setenv(k, v); err != nil {
t.Fatalf("Failed to set env %s: %v", k, err)
}
}
// Return cleanup function
return func() {
for k, v := range original {
if v == "" {
os.Unsetenv(k)
} else {
os.Setenv(k, v)
}
}
}
}
func TestLoadConfig_Success(t *testing.T) {
cleanup := setupEnv(t, map[string]string{
"HOLD_SERVER_PUBLIC_URL": "https://hold.example.com",
"HOLD_SERVER_ADDR": ":9000",
"HOLD_SERVER_PUBLIC": "true",
"HOLD_SERVER_TEST_MODE": "true",
"HOLD_REGISTRATION_OWNER_DID": "did:plc:owner123",
"HOLD_REGISTRATION_ALLOW_ALL_CREW": "true",
"S3_BUCKET": "test-bucket",
"AWS_ACCESS_KEY_ID": "test-key",
"AWS_SECRET_ACCESS_KEY": "test-secret",
"HOLD_DATABASE_PATH": "/tmp/test-db",
"HOLD_DATABASE_KEY_PATH": "/tmp/test-key.pem",
})
defer cleanup()
cfg, err := LoadConfig("")
if err != nil {
t.Fatalf("Expected success, got error: %v", err)
}
// Verify server config
if cfg.Server.PublicURL != "https://hold.example.com" {
t.Errorf("Expected PublicURL=https://hold.example.com, got %s", cfg.Server.PublicURL)
}
if cfg.Server.Addr != ":9000" {
t.Errorf("Expected Addr=:9000, got %s", cfg.Server.Addr)
}
if !cfg.Server.Public {
t.Error("Expected Public=true")
}
if !cfg.Server.TestMode {
t.Error("Expected TestMode=true")
}
if cfg.Server.ReadTimeout != 5*time.Minute {
t.Errorf("Expected ReadTimeout=5m, got %v", cfg.Server.ReadTimeout)
}
// Verify registration config
if cfg.Registration.OwnerDID != "did:plc:owner123" {
t.Errorf("Expected OwnerDID=did:plc:owner123, got %s", cfg.Registration.OwnerDID)
}
if !cfg.Registration.AllowAllCrew {
t.Error("Expected AllowAllCrew=true")
}
// Verify database config
if cfg.Database.Path != "/tmp/test-db" {
t.Errorf("Expected Database.Path=/tmp/test-db, got %s", cfg.Database.Path)
}
if cfg.Database.KeyPath != "/tmp/test-key.pem" {
t.Errorf("Expected Database.KeyPath=/tmp/test-key.pem, got %s", cfg.Database.KeyPath)
}
}
func TestLoadConfig_MissingPublicURL(t *testing.T) {
cleanup := setupEnv(t, map[string]string{
"HOLD_SERVER_PUBLIC_URL": "", // Missing required field
"S3_BUCKET": "test-bucket",
})
defer cleanup()
_, err := LoadConfig("")
if err == nil {
t.Error("Expected error for missing HOLD_SERVER_PUBLIC_URL")
}
}
func TestLoadConfig_MissingS3Bucket(t *testing.T) {
cleanup := setupEnv(t, map[string]string{
"HOLD_SERVER_PUBLIC_URL": "https://hold.example.com",
"S3_BUCKET": "", // Missing required field
})
defer cleanup()
_, err := LoadConfig("")
if err == nil {
t.Error("Expected error for missing S3_BUCKET")
}
}
func TestLoadConfig_Defaults(t *testing.T) {
cleanup := setupEnv(t, map[string]string{
"HOLD_SERVER_PUBLIC_URL": "https://hold.example.com",
"S3_BUCKET": "test-bucket",
"AWS_ACCESS_KEY_ID": "test-key",
"AWS_SECRET_ACCESS_KEY": "test-secret",
// Don't set optional vars - test defaults
"HOLD_SERVER_ADDR": "",
"HOLD_SERVER_PUBLIC": "",
"HOLD_SERVER_TEST_MODE": "",
"HOLD_REGISTRATION_OWNER_DID": "",
"HOLD_REGISTRATION_ALLOW_ALL_CREW": "",
"AWS_REGION": "",
"HOLD_DATABASE_PATH": "",
})
defer cleanup()
cfg, err := LoadConfig("")
if err != nil {
t.Fatalf("Expected success, got error: %v", err)
}
// Verify defaults
if cfg.Server.Addr != ":8080" {
t.Errorf("Expected default Addr=:8080, got %s", cfg.Server.Addr)
}
if cfg.Server.Public {
t.Error("Expected default Public=false")
}
if cfg.Server.TestMode {
t.Error("Expected default TestMode=false")
}
if cfg.Registration.OwnerDID != "" {
t.Error("Expected default OwnerDID to be empty")
}
if cfg.Registration.AllowAllCrew {
t.Error("Expected default AllowAllCrew=false")
}
if cfg.Database.Path != "/var/lib/atcr-hold" {
t.Errorf("Expected default Database.Path=/var/lib/atcr-hold, got %s", cfg.Database.Path)
}
}
func TestLoadConfig_KeyPathDefault(t *testing.T) {
cleanup := setupEnv(t, map[string]string{
"HOLD_SERVER_PUBLIC_URL": "https://hold.example.com",
"S3_BUCKET": "test-bucket",
"AWS_ACCESS_KEY_ID": "test-key",
"AWS_SECRET_ACCESS_KEY": "test-secret",
"HOLD_DATABASE_PATH": "/custom/db/path",
"HOLD_DATABASE_KEY_PATH": "", // Should default to {Database.Path}/signing.key
})
defer cleanup()
cfg, err := LoadConfig("")
if err != nil {
t.Fatalf("Expected success, got error: %v", err)
}
expectedKeyPath := filepath.Join("/custom/db/path", "signing.key")
if cfg.Database.KeyPath != expectedKeyPath {
t.Errorf("Expected KeyPath=%s, got %s", expectedKeyPath, cfg.Database.KeyPath)
}
}
func TestS3Params_Complete(t *testing.T) {
sc := StorageConfig{
AccessKey: "test-access-key",
SecretKey: "test-secret-key",
Region: "us-west-2",
Bucket: "test-bucket",
Endpoint: "https://s3.example.com",
}
params := sc.S3Params()
if params["accesskey"] != "test-access-key" {
t.Errorf("Expected accesskey=test-access-key, got %v", params["accesskey"])
}
if params["secretkey"] != "test-secret-key" {
t.Errorf("Expected secretkey=test-secret-key, got %v", params["secretkey"])
}
if params["region"] != "us-west-2" {
t.Errorf("Expected region=us-west-2, got %v", params["region"])
}
if params["bucket"] != "test-bucket" {
t.Errorf("Expected bucket=test-bucket, got %v", params["bucket"])
}
if params["regionendpoint"] != "https://s3.example.com" {
t.Errorf("Expected regionendpoint=https://s3.example.com, got %v", params["regionendpoint"])
}
}
func TestS3Params_NoEndpoint(t *testing.T) {
sc := StorageConfig{
AccessKey: "test-key",
SecretKey: "test-secret",
Region: "us-east-1",
Bucket: "test-bucket",
Endpoint: "", // No custom endpoint
}
params := sc.S3Params()
// Should have default region
if params["region"] != "us-east-1" {
t.Errorf("Expected default region=us-east-1, got %v", params["region"])
}
// Should not have regionendpoint
if _, exists := params["regionendpoint"]; exists {
t.Error("Expected no regionendpoint when Endpoint not set")
}
}
func TestDefaultConfig_Hold(t *testing.T) {
cfg := DefaultConfig()
if cfg.Version != "0.1" {
t.Errorf("DefaultConfig().Version = %q, want \"0.1\"", cfg.Version)
}
if cfg.LogLevel != "info" {
t.Errorf("DefaultConfig().LogLevel = %q, want \"info\"", cfg.LogLevel)
}
if cfg.Server.Addr != ":8080" {
t.Errorf("DefaultConfig().Server.Addr = %q, want \":8080\"", cfg.Server.Addr)
}
if cfg.Storage.Region != "us-east-1" {
t.Errorf("DefaultConfig().Storage.Region = %q, want \"us-east-1\"", cfg.Storage.Region)
}
if cfg.Database.Path != "/var/lib/atcr-hold" {
t.Errorf("DefaultConfig().Database.Path = %q, want \"/var/lib/atcr-hold\"", cfg.Database.Path)
}
if cfg.Server.ReadTimeout != 5*time.Minute {
t.Errorf("DefaultConfig().Server.ReadTimeout = %v, want 5m", cfg.Server.ReadTimeout)
}
}
func TestExampleYAML_Hold(t *testing.T) {
out, err := ExampleYAML()
if err != nil {
t.Fatalf("ExampleYAML() error: %v", err)
}
s := string(out)
// Should contain the title
if !strings.Contains(s, "ATCR Hold Service Configuration") {
t.Error("expected title in YAML output")
}
// Should contain key fields with defaults
if !strings.Contains(s, "addr:") {
t.Error("expected addr field in YAML output")
}
if !strings.Contains(s, "bucket:") {
t.Error("expected bucket field in YAML output")
}
// Should contain comments
if !strings.Contains(s, "# Listen address") {
t.Error("expected comment for addr field")
}
if !strings.Contains(s, "# S3 bucket") {
t.Error("expected comment for bucket field")
}
}