add new upcloud cli deploy

This commit is contained in:
Evan Jarrett
2026-02-07 22:45:10 -06:00
parent ef0161fb0e
commit cd47945301
30 changed files with 2328 additions and 43 deletions
+5 -5
View File
@@ -57,8 +57,8 @@ type ServerConfig struct {
// Short name used in page titles and browser tabs.
ClientShortName string `yaml:"client_short_name" comment:"Short name used in page titles and browser tabs."`
// Separate domain for OCI registry API.
RegistryDomain string `yaml:"registry_domain" comment:"Separate domain for OCI registry API (e.g. \"buoy.cr\"). Browser visits redirect to BaseURL."`
// Separate domains for OCI registry API. First entry is the primary (used for JWT service name and UI display).
RegistryDomains []string `yaml:"registry_domains" comment:"Separate domains for OCI registry API (e.g. [\"buoy.cr\"]). First is primary. Browser visits redirect to BaseURL."`
}
// UIConfig defines web UI settings
@@ -145,7 +145,7 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("server.client_name", "AT Container Registry")
v.SetDefault("server.client_short_name", "ATCR")
v.SetDefault("server.oauth_key_path", "/var/lib/atcr/oauth/client.key")
v.SetDefault("server.registry_domain", "")
v.SetDefault("server.registry_domains", []string{})
// UI defaults
v.SetDefault("ui.database_path", "/var/lib/atcr/ui.db")
@@ -241,8 +241,8 @@ func LoadConfig(yamlPath string) (*Config, error) {
// deriveServiceName extracts the JWT service name from the config.
func deriveServiceName(cfg *Config) string {
if cfg.Server.RegistryDomain != "" {
return cfg.Server.RegistryDomain
if len(cfg.Server.RegistryDomains) > 0 {
return cfg.Server.RegistryDomains[0]
}
return getServiceName(cfg.Server.BaseURL)
}
+2
View File
@@ -87,6 +87,8 @@ func setupHoldTestDB(t *testing.T) *sql.DB {
}
// Limit to single connection to avoid race conditions in tests
db.SetMaxOpenConns(1)
// Clean slate: shared-cache in-memory DB may retain data from prior subtests
db.Exec("DELETE FROM hold_captain_records")
t.Cleanup(func() { db.Close() })
return db
}
+25 -9
View File
@@ -240,10 +240,10 @@ func NewAppViewServer(cfg *Config, branding *BrandingOverrides) (*AppViewServer,
mainRouter.Use(routes.CORSMiddleware())
// Registry domain redirect middleware
if cfg.Server.RegistryDomain != "" {
mainRouter.Use(RegistryDomainRedirect(cfg.Server.RegistryDomain, cfg.Server.BaseURL))
if len(cfg.Server.RegistryDomains) > 0 {
mainRouter.Use(RegistryDomainRedirect(cfg.Server.RegistryDomains, cfg.Server.BaseURL))
slog.Info("Registry domain redirect enabled",
"registry_domain", cfg.Server.RegistryDomain,
"registry_domains", cfg.Server.RegistryDomains,
"ui_base_url", cfg.Server.BaseURL)
}
@@ -263,7 +263,7 @@ func NewAppViewServer(cfg *Config, branding *BrandingOverrides) (*AppViewServer,
OAuthStore: s.OAuthStore,
Refresher: s.Refresher,
BaseURL: baseURL,
RegistryDomain: cfg.Server.RegistryDomain,
RegistryDomain: primaryRegistryDomain(cfg.Server.RegistryDomains),
DeviceStore: s.DeviceStore,
HealthChecker: s.HealthChecker,
ReadmeFetcher: s.ReadmeFetcher,
@@ -499,7 +499,10 @@ func NewAppViewServer(cfg *Config, branding *BrandingOverrides) (*AppViewServer,
mainRouter.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
if err := json.NewEncoder(w).Encode(map[string]string{"status": "ok"}); err != nil {
http.Error(w, "encode error", http.StatusInternalServerError)
return
}
})
// Register credential helper version API (public endpoint)
@@ -577,10 +580,15 @@ func (s *AppViewServer) createTokenIssuer() (*token.Issuer, error) {
)
}
// RegistryDomainRedirect redirects all non-registry requests from the registry
// domain to the UI domain. Only /v2 and /v2/* pass through for Docker clients.
// RegistryDomainRedirect redirects all non-registry requests from registry
// domains to the UI domain. Only /v2 and /v2/* pass through for Docker clients.
// Uses 307 (Temporary Redirect) to preserve POST method/body.
func RegistryDomainRedirect(registryDomain, uiBaseURL string) func(http.Handler) http.Handler {
func RegistryDomainRedirect(registryDomains []string, uiBaseURL string) func(http.Handler) http.Handler {
domains := make(map[string]bool, len(registryDomains))
for _, d := range registryDomains {
domains[d] = true
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := r.Host
@@ -588,7 +596,7 @@ func RegistryDomainRedirect(registryDomain, uiBaseURL string) func(http.Handler)
host = host[:idx]
}
if host == registryDomain {
if domains[host] {
path := r.URL.Path
if path == "/v2" || path == "/v2/" || strings.HasPrefix(path, "/v2/") {
next.ServeHTTP(w, r)
@@ -605,6 +613,14 @@ func RegistryDomainRedirect(registryDomain, uiBaseURL string) func(http.Handler)
}
}
// primaryRegistryDomain returns the first registry domain, or empty string if none.
func primaryRegistryDomain(domains []string) string {
if len(domains) > 0 {
return domains[0]
}
return ""
}
// initializeJetstream initializes the Jetstream workers for real-time events and backfill.
func (s *AppViewServer) initializeJetstream() {
jetstreamURL := s.Config.Jetstream.URL
+16
View File
@@ -131,6 +131,22 @@ func valueToNode(v reflect.Value) (*yaml.Node, error) {
return mapToNode(v)
}
// Slice → yaml sequence
if v.Kind() == reflect.Slice {
seq := &yaml.Node{
Kind: yaml.SequenceNode,
Tag: "!!seq",
}
for i := 0; i < v.Len(); i++ {
elemNode, err := valueToNode(v.Index(i))
if err != nil {
return nil, fmt.Errorf("slice index %d: %w", i, err)
}
seq.Content = append(seq.Content, elemNode)
}
return seq, nil
}
// Scalar types
node := &yaml.Node{Kind: yaml.ScalarNode}
switch v.Kind() {
+1 -1
View File
@@ -114,7 +114,7 @@ func (h *XRPCHandler) HandleGetSubscriptionInfo(w http.ResponseWriter, r *http.R
stats, err := h.pdsServer.GetQuotaForUserWithTier(r.Context(), userDID, h.manager.quotaMgr)
if err == nil {
info.CurrentUsage = stats.TotalSize
info.CrewTier = stats.Tier // tier from local crew record (what's actually enforced)
info.CrewTier = stats.Tier // tier from local crew record (what's actually enforced)
info.CurrentLimit = stats.Limit
// If no subscription but crew has a tier, show that as current
+7 -8
View File
@@ -1,17 +1,16 @@
// Package db contains a vendored from github.com/bluesky-social/indigo/carstore/sqlite_store.go
// Package db contains a vendored from github.com/bluesky-social/indigo/carstore/sqlite_store.go
// Source: github.com/bluesky-social/indigo@v0.0.0-20260203235305-a86f3ae1f8ec/carstore/
// Reason: indigo's carstore hardcodes mattn/go-sqlite3, which conflicts with go-libsql
// (both bundle SQLite C libraries and cannot coexist in the same binary).
//
// This package replaces the mattn driver with go-libsql and removes Prometheus metrics.
// Once upstream accepts a driver-agnostic constructor, this vendored copy can be removed.
// Modifications:
// - Replaced mattn/go-sqlite3 driver with go-libsql
// - Removed all Prometheus metric counters and .Inc() calls
// - Changed package from 'carstore' to 'db'
// - Added NewSQLiteStoreWithDB constructor for injecting an existing *sql.DB
// - Changed sql.Open("sqlite3", path) to sql.Open("libsql", ...) with proper DSN
// Modifications:
// - Replaced mattn/go-sqlite3 driver with go-libsql
// - Removed all Prometheus metric counters and .Inc() calls
// - Changed package from 'carstore' to 'db'
// - Added NewSQLiteStoreWithDB constructor for injecting an existing *sql.DB
// - Changed sql.Open("sqlite3", path) to sql.Open("libsql", ...) with proper DSN
package db
import (