mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-03 08:46:57 +00:00
fix labeler deployment
This commit is contained in:
@@ -11,8 +11,12 @@ labeler:
|
||||
enabled: true
|
||||
# Listen address for labeler (e.g., :5002).
|
||||
addr: :5002
|
||||
# Externally reachable labeler URL. Empty = derive from server.base_url.
|
||||
public_url: ""
|
||||
# Externally reachable labeler URL (required, e.g. https://labeler.example.com).
|
||||
public_url: https://labeler.example.com
|
||||
# OAuth client display name (e.g., "ATCR Labeler").
|
||||
client_name: ATCR Labeler
|
||||
# Short brand label used in UI copy (e.g., "ATCR").
|
||||
client_short_name: ATCR
|
||||
# DID of the labeler admin. Only this DID can log into the admin panel.
|
||||
owner_did: did:plc:your-did-here
|
||||
# Directory for labeler state (database, signing key, did.txt).
|
||||
@@ -33,12 +37,6 @@ labeler:
|
||||
libsql_auth_token: ""
|
||||
# Embedded-replica pull interval (e.g. 30s). 0 = manual sync only.
|
||||
libsql_sync_interval: 0s
|
||||
# AppView server settings (shared config).
|
||||
server:
|
||||
base_url: https://atcr.io
|
||||
client_name: AT Container Registry
|
||||
client_short_name: ATCR
|
||||
test_mode: false
|
||||
# Remote log shipping settings.
|
||||
log_shipper:
|
||||
# Log shipping backend: "victoria", "opensearch", or "loki". Empty disables shipping.
|
||||
|
||||
@@ -49,10 +49,11 @@ type ConfigValues struct {
|
||||
S3SecretKey string
|
||||
|
||||
// Infrastructure (computed from zone + config)
|
||||
Zone string // e.g. "us-chi1"
|
||||
HoldDomain string // e.g. "us-chi1.cove.seamark.dev"
|
||||
HoldDid string // e.g. "did:web:us-chi1.cove.seamark.dev"
|
||||
BasePath string // e.g. "/var/lib/seamark"
|
||||
Zone string // e.g. "us-chi1"
|
||||
HoldDomain string // e.g. "us-chi1.cove.seamark.dev"
|
||||
HoldDid string // e.g. "did:web:us-chi1.cove.seamark.dev"
|
||||
LabelerDomain string // e.g. "labeler.seamark.dev"
|
||||
BasePath string // e.g. "/var/lib/seamark"
|
||||
|
||||
// Scanner (auto-generated shared secret)
|
||||
ScannerSecret string // hex-encoded 32-byte secret; empty disables scanning
|
||||
@@ -373,8 +374,10 @@ func generateCloudInit(p cloudInitParams) (string, error) {
|
||||
}
|
||||
|
||||
// syncServiceUnit compares a rendered systemd service unit against what's on
|
||||
// the server. If they differ, it writes the new unit file. Returns true if the
|
||||
// unit was updated (caller should daemon-reload before restart).
|
||||
// the server. If they differ, it writes the new unit file. If the unit is
|
||||
// missing entirely, it installs it and runs `systemctl enable` so the service
|
||||
// starts on boot. Returns true if the unit was created or updated (caller
|
||||
// should daemon-reload before restart).
|
||||
func syncServiceUnit(name, ip, serviceName, renderedUnit string) (bool, error) {
|
||||
unitPath := "/etc/systemd/system/" + serviceName + ".service"
|
||||
|
||||
@@ -387,8 +390,15 @@ func syncServiceUnit(name, ip, serviceName, renderedUnit string) (bool, error) {
|
||||
rendered := strings.TrimSpace(renderedUnit)
|
||||
|
||||
if remote == "__MISSING__" {
|
||||
fmt.Printf(" service unit: %s not found (cloud-init will handle it)\n", name)
|
||||
return false, nil
|
||||
// First-time install: write file, daemon-reload, and enable so the
|
||||
// service comes up on boot. The caller's restart will start it.
|
||||
script := fmt.Sprintf("cat > %s << 'SVCEOF'\n%s\nSVCEOF\nsystemctl daemon-reload\nsystemctl enable %s",
|
||||
unitPath, rendered, serviceName)
|
||||
if _, err := runSSH(ip, script, false); err != nil {
|
||||
return false, fmt.Errorf("install service unit: %w", err)
|
||||
}
|
||||
fmt.Printf(" service unit: %s installed and enabled\n", name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if remote == rendered {
|
||||
@@ -416,7 +426,17 @@ func syncConfigKeys(name, ip, configPath, templateYAML string) error {
|
||||
remote = strings.TrimSpace(remote)
|
||||
|
||||
if remote == "__MISSING__" {
|
||||
fmt.Printf(" config sync: %s not yet created (cloud-init will handle it)\n", name)
|
||||
// First-time install: write the rendered template as-is. Subsequent
|
||||
// runs use the merge-keys path below to preserve operator edits.
|
||||
dir := configPath[:strings.LastIndex(configPath, "/")]
|
||||
if _, err := runSSH(ip, fmt.Sprintf("mkdir -p %s", dir), false); err != nil {
|
||||
return fmt.Errorf("create config dir: %w", err)
|
||||
}
|
||||
script := fmt.Sprintf("cat > %s << 'CFGEOF'\n%s\nCFGEOF", configPath, strings.TrimRight(templateYAML, "\n"))
|
||||
if _, err := runSSH(ip, script, false); err != nil {
|
||||
return fmt.Errorf("write initial config: %w", err)
|
||||
}
|
||||
fmt.Printf(" config sync: %s installed\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -90,10 +90,12 @@ func extractFromAppviewTemplate() (clientName, baseDomain string, registryDomain
|
||||
return clientName, baseDomain, registryDomains, nil
|
||||
}
|
||||
|
||||
// readSSHPublicKey reads an SSH public key from a file path.
|
||||
// readSSHPublicKey reads an SSH public key from a file path. An empty path
|
||||
// returns an empty key without error — callers that need the key (e.g. when
|
||||
// creating new servers) must check for empty before use.
|
||||
func readSSHPublicKey(path string) (string, error) {
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("--ssh-key is required (path to SSH public key file)")
|
||||
return "", nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,9 @@ log_shipper:
|
||||
labeler:
|
||||
enabled: true
|
||||
addr: :5002
|
||||
public_url: "https://{{.LabelerDomain}}"
|
||||
client_name: "Seamark Labeler"
|
||||
client_short_name: Seamark
|
||||
owner_did: ""
|
||||
data_dir: "{{.BasePath}}/labeler"
|
||||
did_method: plc
|
||||
@@ -17,8 +20,3 @@ labeler:
|
||||
key_path: ""
|
||||
rotation_key: ""
|
||||
plc_directory_url: https://plc.directory
|
||||
server:
|
||||
base_url: "https://seamark.dev"
|
||||
client_name: Seamark
|
||||
client_short_name: Seamark
|
||||
test_mode: false
|
||||
|
||||
+250
-73
@@ -37,11 +37,10 @@ var provisionCmd = &cobra.Command{
|
||||
func init() {
|
||||
provisionCmd.Flags().String("zone", "", "UpCloud zone (interactive picker if omitted)")
|
||||
provisionCmd.Flags().String("plan", "", "Server plan (interactive picker if omitted)")
|
||||
provisionCmd.Flags().String("ssh-key", "", "Path to SSH public key file (required)")
|
||||
provisionCmd.Flags().String("ssh-key", "", "Path to SSH public key file (required when creating new servers)")
|
||||
provisionCmd.Flags().String("s3-secret", "", "S3 secret access key (for existing object storage)")
|
||||
provisionCmd.Flags().Bool("with-scanner", false, "Deploy vulnerability scanner alongside hold")
|
||||
provisionCmd.Flags().Bool("with-labeler", false, "Deploy content moderation labeler alongside appview")
|
||||
_ = provisionCmd.MarkFlagRequired("ssh-key")
|
||||
rootCmd.AddCommand(provisionCmd)
|
||||
}
|
||||
|
||||
@@ -154,6 +153,8 @@ func cmdProvision(token, zone, plan, sshKeyPath, s3Secret string, withScanner, w
|
||||
|
||||
// Hold domain is zone-based (e.g. us-chi1.cove.seamark.dev)
|
||||
holdDomain := cfg.Zone + ".cove." + cfg.BaseDomain
|
||||
// Labeler domain is a fixed subdomain on the base domain (e.g. labeler.seamark.dev)
|
||||
labelerDomain := "labeler." + cfg.BaseDomain
|
||||
|
||||
// Build config template values
|
||||
vals := &ConfigValues{
|
||||
@@ -165,6 +166,7 @@ func cmdProvision(token, zone, plan, sshKeyPath, s3Secret string, withScanner, w
|
||||
Zone: cfg.Zone,
|
||||
HoldDomain: holdDomain,
|
||||
HoldDid: "did:web:" + holdDomain,
|
||||
LabelerDomain: labelerDomain,
|
||||
BasePath: naming.BasePath(),
|
||||
ScannerSecret: state.ScannerSecret,
|
||||
}
|
||||
@@ -307,7 +309,7 @@ func cmdProvision(token, zone, plan, sshKeyPath, s3Secret string, withScanner, w
|
||||
fmt.Printf("Load balancer: %s (exists)\n", state.LB.UUID)
|
||||
} else {
|
||||
fmt.Println("Creating load balancer (Essentials tier)...")
|
||||
lb, err := createLoadBalancer(ctx, svc, cfg, naming, state.Network.UUID, state.Appview.PrivateIP, state.Hold.PrivateIP, holdDomain)
|
||||
lb, err := createLoadBalancer(ctx, svc, cfg, naming, state.Network.UUID, state.Appview.PrivateIP, state.Hold.PrivateIP, holdDomain, labelerDomain, state.LabelerEnabled)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create LB: %w", err)
|
||||
}
|
||||
@@ -325,6 +327,13 @@ func cmdProvision(token, zone, plan, sshKeyPath, s3Secret string, withScanner, w
|
||||
return fmt.Errorf("LB hold forwarded headers: %w", err)
|
||||
}
|
||||
|
||||
// Ensure labeler backend + route-labeler rule when labeler is enabled
|
||||
if state.LabelerEnabled {
|
||||
if err := ensureLBLabelerRoute(ctx, svc, state.LB.UUID, state.Appview.PrivateIP, labelerDomain); err != nil {
|
||||
return fmt.Errorf("LB labeler route: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Always reconcile scanner block rule
|
||||
if err := ensureLBScannerBlock(ctx, svc, state.LB.UUID); err != nil {
|
||||
return fmt.Errorf("LB scanner block: %w", err)
|
||||
@@ -334,6 +343,9 @@ func cmdProvision(token, zone, plan, sshKeyPath, s3Secret string, withScanner, w
|
||||
tlsDomains := []string{cfg.BaseDomain}
|
||||
tlsDomains = append(tlsDomains, cfg.RegistryDomains...)
|
||||
tlsDomains = append(tlsDomains, holdDomain)
|
||||
if state.LabelerEnabled {
|
||||
tlsDomains = append(tlsDomains, labelerDomain)
|
||||
}
|
||||
if err := ensureLBCertificates(ctx, svc, state.LB.UUID, tlsDomains); err != nil {
|
||||
return fmt.Errorf("LB certificates: %w", err)
|
||||
}
|
||||
@@ -428,6 +440,25 @@ func cmdProvision(token, zone, plan, sshKeyPath, s3Secret string, withScanner, w
|
||||
}
|
||||
}
|
||||
|
||||
// Labeler binary: build and upload when labeler is enabled but appview was
|
||||
// not freshly created (the appviewCreated branch above already handled it).
|
||||
if state.LabelerEnabled && !appviewCreated {
|
||||
rootDir := projectRoot()
|
||||
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 {
|
||||
return fmt.Errorf("build labeler: %w", err)
|
||||
}
|
||||
labelerRemote := naming.InstallDir() + "/bin/" + naming.Labeler()
|
||||
fmt.Println("Deploying labeler binary...")
|
||||
if err := scpFile(labelerLocal, state.Appview.PublicIP, labelerRemote); err != nil {
|
||||
return fmt.Errorf("upload labeler: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Provisioning Complete ===")
|
||||
fmt.Println()
|
||||
fmt.Println("DNS records needed:")
|
||||
@@ -437,6 +468,9 @@ func cmdProvision(token, zone, plan, sshKeyPath, s3Secret string, withScanner, w
|
||||
fmt.Printf(" CNAME %-24s → %s\n", rd, lbDNS)
|
||||
}
|
||||
fmt.Printf(" CNAME %-24s → %s\n", holdDomain, lbDNS)
|
||||
if state.LabelerEnabled {
|
||||
fmt.Printf(" CNAME %-24s → %s\n", labelerDomain, lbDNS)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(" (LB DNS name not yet available — check 'status' in a few minutes)")
|
||||
}
|
||||
@@ -574,6 +608,9 @@ func objectStorageRegion(zone string) string {
|
||||
}
|
||||
|
||||
func createServer(ctx context.Context, svc *service.Service, cfg *InfraConfig, templateUUID, networkUUID, title, userData string) (*ServerState, error) {
|
||||
if cfg.SSHPublicKey == "" {
|
||||
return nil, fmt.Errorf("creating server %s requires --ssh-key (path to SSH public key file)", title)
|
||||
}
|
||||
storageTier := "maxiops"
|
||||
if strings.HasPrefix(strings.ToUpper(cfg.Plan), "DEV-") {
|
||||
storageTier = "standard"
|
||||
@@ -709,7 +746,83 @@ func createFirewallRules(ctx context.Context, svc *service.Service, serverUUID,
|
||||
})
|
||||
}
|
||||
|
||||
func createLoadBalancer(ctx context.Context, svc *service.Service, cfg *InfraConfig, naming Naming, networkUUID, appviewIP, holdIP, holdDomain string) (*upcloud.LoadBalancer, error) {
|
||||
func createLoadBalancer(ctx context.Context, svc *service.Service, cfg *InfraConfig, naming Naming, networkUUID, appviewIP, holdIP, holdDomain, labelerDomain string, withLabeler bool) (*upcloud.LoadBalancer, error) {
|
||||
frontendRules := []request.LoadBalancerFrontendRule{
|
||||
{
|
||||
Name: "set-forwarded-headers",
|
||||
Priority: 1,
|
||||
Matchers: []upcloud.LoadBalancerMatcher{},
|
||||
Actions: []upcloud.LoadBalancerAction{
|
||||
request.NewLoadBalancerSetForwardedHeadersAction(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "route-hold",
|
||||
Priority: 10,
|
||||
Matchers: []upcloud.LoadBalancerMatcher{
|
||||
{
|
||||
Type: upcloud.LoadBalancerMatcherTypeHost,
|
||||
Host: &upcloud.LoadBalancerMatcherHost{
|
||||
Value: holdDomain,
|
||||
},
|
||||
},
|
||||
},
|
||||
Actions: []upcloud.LoadBalancerAction{
|
||||
request.NewLoadBalancerSetForwardedHeadersAction(),
|
||||
{
|
||||
Type: upcloud.LoadBalancerActionTypeUseBackend,
|
||||
UseBackend: &upcloud.LoadBalancerActionUseBackend{
|
||||
Backend: "hold",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
backends := []request.LoadBalancerBackend{
|
||||
{
|
||||
Name: "appview",
|
||||
Members: []request.LoadBalancerBackendMember{
|
||||
{
|
||||
Name: "appview-1",
|
||||
Type: upcloud.LoadBalancerBackendMemberTypeStatic,
|
||||
IP: appviewIP,
|
||||
Port: 5000,
|
||||
Weight: 100,
|
||||
MaxSessions: 1000,
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
Properties: &upcloud.LoadBalancerBackendProperties{
|
||||
HealthCheckType: upcloud.LoadBalancerHealthCheckTypeHTTP,
|
||||
HealthCheckURL: "/health",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "hold",
|
||||
Members: []request.LoadBalancerBackendMember{
|
||||
{
|
||||
Name: "hold-1",
|
||||
Type: upcloud.LoadBalancerBackendMemberTypeStatic,
|
||||
IP: holdIP,
|
||||
Port: 8080,
|
||||
Weight: 100,
|
||||
MaxSessions: 1000,
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
Properties: &upcloud.LoadBalancerBackendProperties{
|
||||
HealthCheckType: upcloud.LoadBalancerHealthCheckTypeHTTP,
|
||||
HealthCheckURL: "/xrpc/_health",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if withLabeler {
|
||||
frontendRules = append(frontendRules, labelerFrontendRule(labelerDomain))
|
||||
backends = append(backends, labelerBackend(appviewIP))
|
||||
}
|
||||
|
||||
lb, err := svc.CreateLoadBalancer(ctx, &request.CreateLoadBalancerRequest{
|
||||
Name: naming.LBName(),
|
||||
Plan: "essentials",
|
||||
@@ -737,37 +850,7 @@ func createLoadBalancer(ctx context.Context, svc *service.Service, cfg *InfraCon
|
||||
Networks: []upcloud.LoadBalancerFrontendNetwork{
|
||||
{Name: "public"},
|
||||
},
|
||||
Rules: []request.LoadBalancerFrontendRule{
|
||||
{
|
||||
Name: "set-forwarded-headers",
|
||||
Priority: 1,
|
||||
Matchers: []upcloud.LoadBalancerMatcher{},
|
||||
Actions: []upcloud.LoadBalancerAction{
|
||||
request.NewLoadBalancerSetForwardedHeadersAction(),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "route-hold",
|
||||
Priority: 10,
|
||||
Matchers: []upcloud.LoadBalancerMatcher{
|
||||
{
|
||||
Type: upcloud.LoadBalancerMatcherTypeHost,
|
||||
Host: &upcloud.LoadBalancerMatcherHost{
|
||||
Value: holdDomain,
|
||||
},
|
||||
},
|
||||
},
|
||||
Actions: []upcloud.LoadBalancerAction{
|
||||
request.NewLoadBalancerSetForwardedHeadersAction(),
|
||||
{
|
||||
Type: upcloud.LoadBalancerActionTypeUseBackend,
|
||||
UseBackend: &upcloud.LoadBalancerActionUseBackend{
|
||||
Backend: "hold",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Rules: frontendRules,
|
||||
},
|
||||
{
|
||||
Name: "http-redirect",
|
||||
@@ -803,44 +886,7 @@ func createLoadBalancer(ctx context.Context, svc *service.Service, cfg *InfraCon
|
||||
},
|
||||
},
|
||||
Resolvers: []request.LoadBalancerResolver{},
|
||||
Backends: []request.LoadBalancerBackend{
|
||||
{
|
||||
Name: "appview",
|
||||
Members: []request.LoadBalancerBackendMember{
|
||||
{
|
||||
Name: "appview-1",
|
||||
Type: upcloud.LoadBalancerBackendMemberTypeStatic,
|
||||
IP: appviewIP,
|
||||
Port: 5000,
|
||||
Weight: 100,
|
||||
MaxSessions: 1000,
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
Properties: &upcloud.LoadBalancerBackendProperties{
|
||||
HealthCheckType: upcloud.LoadBalancerHealthCheckTypeHTTP,
|
||||
HealthCheckURL: "/health",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "hold",
|
||||
Members: []request.LoadBalancerBackendMember{
|
||||
{
|
||||
Name: "hold-1",
|
||||
Type: upcloud.LoadBalancerBackendMemberTypeStatic,
|
||||
IP: holdIP,
|
||||
Port: 8080,
|
||||
Weight: 100,
|
||||
MaxSessions: 1000,
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
Properties: &upcloud.LoadBalancerBackendProperties{
|
||||
HealthCheckType: upcloud.LoadBalancerHealthCheckTypeHTTP,
|
||||
HealthCheckURL: "/xrpc/_health",
|
||||
},
|
||||
},
|
||||
},
|
||||
Backends: backends,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -849,6 +895,55 @@ func createLoadBalancer(ctx context.Context, svc *service.Service, cfg *InfraCon
|
||||
return lb, nil
|
||||
}
|
||||
|
||||
// labelerBackend builds the labeler LB backend pointing at the appview server's
|
||||
// private IP on the labeler listen port.
|
||||
func labelerBackend(appviewIP string) request.LoadBalancerBackend {
|
||||
return request.LoadBalancerBackend{
|
||||
Name: "labeler",
|
||||
Members: []request.LoadBalancerBackendMember{
|
||||
{
|
||||
Name: "labeler-1",
|
||||
Type: upcloud.LoadBalancerBackendMemberTypeStatic,
|
||||
IP: appviewIP,
|
||||
Port: 5002,
|
||||
Weight: 100,
|
||||
MaxSessions: 1000,
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
Properties: &upcloud.LoadBalancerBackendProperties{
|
||||
HealthCheckType: upcloud.LoadBalancerHealthCheckTypeHTTP,
|
||||
HealthCheckURL: "/.well-known/did.json",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// labelerFrontendRule returns a host-match rule routing labelerDomain to the
|
||||
// labeler backend with forwarded headers.
|
||||
func labelerFrontendRule(labelerDomain string) request.LoadBalancerFrontendRule {
|
||||
return request.LoadBalancerFrontendRule{
|
||||
Name: "route-labeler",
|
||||
Priority: 20,
|
||||
Matchers: []upcloud.LoadBalancerMatcher{
|
||||
{
|
||||
Type: upcloud.LoadBalancerMatcherTypeHost,
|
||||
Host: &upcloud.LoadBalancerMatcherHost{
|
||||
Value: labelerDomain,
|
||||
},
|
||||
},
|
||||
},
|
||||
Actions: []upcloud.LoadBalancerAction{
|
||||
request.NewLoadBalancerSetForwardedHeadersAction(),
|
||||
{
|
||||
Type: upcloud.LoadBalancerActionTypeUseBackend,
|
||||
UseBackend: &upcloud.LoadBalancerActionUseBackend{
|
||||
Backend: "labeler",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ensureLBCertificates reconciles TLS certificate bundles on the load balancer.
|
||||
// It skips domains that already have a TLS config attached and creates missing ones.
|
||||
func ensureLBCertificates(ctx context.Context, svc *service.Service, lbUUID string, tlsDomains []string) error {
|
||||
@@ -1026,6 +1121,88 @@ func ensureLBHoldForwardedHeaders(ctx context.Context, svc *service.Service, lbU
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureLBLabelerRoute idempotently ensures the LB has a "labeler" backend
|
||||
// pointing at the appview server's private IP and a "route-labeler" frontend
|
||||
// rule matching labelerDomain. Used to add labeler routing to a pre-existing LB
|
||||
// during a re-provision with --with-labeler.
|
||||
func ensureLBLabelerRoute(ctx context.Context, svc *service.Service, lbUUID, appviewIP, labelerDomain string) error {
|
||||
// 1. Ensure backend exists
|
||||
backends, err := svc.GetLoadBalancerBackends(ctx, &request.GetLoadBalancerBackendsRequest{ServiceUUID: lbUUID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get backends: %w", err)
|
||||
}
|
||||
hasBackend := false
|
||||
for _, b := range backends {
|
||||
if b.Name == "labeler" {
|
||||
hasBackend = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasBackend {
|
||||
_, err := svc.CreateLoadBalancerBackend(ctx, &request.CreateLoadBalancerBackendRequest{
|
||||
ServiceUUID: lbUUID,
|
||||
Backend: labelerBackend(appviewIP),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create labeler backend: %w", err)
|
||||
}
|
||||
fmt.Println(" Labeler backend: created")
|
||||
} else {
|
||||
fmt.Println(" Labeler backend: exists")
|
||||
}
|
||||
|
||||
// 2. Ensure frontend rule exists with correct host matcher
|
||||
rules, err := svc.GetLoadBalancerFrontendRules(ctx, &request.GetLoadBalancerFrontendRulesRequest{
|
||||
ServiceUUID: lbUUID,
|
||||
FrontendName: "https",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get frontend rules: %w", err)
|
||||
}
|
||||
for _, r := range rules {
|
||||
if r.Name == "route-labeler" {
|
||||
// Verify the host matcher and use_backend action are correct
|
||||
hostOK := false
|
||||
for _, m := range r.Matchers {
|
||||
if m.Host != nil && m.Host.Value == labelerDomain {
|
||||
hostOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
backendOK := false
|
||||
for _, a := range r.Actions {
|
||||
if a.UseBackend != nil && a.UseBackend.Backend == "labeler" {
|
||||
backendOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hostOK && backendOK {
|
||||
fmt.Println(" Route-labeler rule: exists and valid")
|
||||
return nil
|
||||
}
|
||||
fmt.Println(" Route-labeler rule: exists but misconfigured, recreating")
|
||||
if err := svc.DeleteLoadBalancerFrontendRule(ctx, &request.DeleteLoadBalancerFrontendRuleRequest{
|
||||
ServiceUUID: lbUUID,
|
||||
FrontendName: "https",
|
||||
Name: r.Name,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("delete route-labeler rule: %w", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := svc.CreateLoadBalancerFrontendRule(ctx, &request.CreateLoadBalancerFrontendRuleRequest{
|
||||
ServiceUUID: lbUUID,
|
||||
FrontendName: "https",
|
||||
Rule: labelerFrontendRule(labelerDomain),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("create route-labeler rule: %w", err)
|
||||
}
|
||||
fmt.Println(" Route-labeler rule: created")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureLBScannerBlock ensures the "https" frontend has a rule that returns 403
|
||||
// for common scanner paths (.php, .asp, .aspx, .jsp, .cgi, .env).
|
||||
func ensureLBScannerBlock(ctx context.Context, svc *service.Service, lbUUID string) error {
|
||||
|
||||
@@ -364,6 +364,7 @@ func configValsFromState(state *InfraState) *ConfigValues {
|
||||
naming := state.Naming()
|
||||
_, baseDomain, _, _ := extractFromAppviewTemplate()
|
||||
holdDomain := state.Zone + ".cove." + baseDomain
|
||||
labelerDomain := "labeler." + baseDomain
|
||||
|
||||
return &ConfigValues{
|
||||
S3Endpoint: state.ObjectStorage.Endpoint,
|
||||
@@ -374,6 +375,7 @@ func configValsFromState(state *InfraState) *ConfigValues {
|
||||
Zone: state.Zone,
|
||||
HoldDomain: holdDomain,
|
||||
HoldDid: "did:web:" + holdDomain,
|
||||
LabelerDomain: labelerDomain,
|
||||
BasePath: naming.BasePath(),
|
||||
ScannerSecret: state.ScannerSecret,
|
||||
}
|
||||
|
||||
+20
-57
@@ -4,7 +4,6 @@ package labeler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,13 +13,12 @@ import (
|
||||
"atcr.io/pkg/config"
|
||||
)
|
||||
|
||||
// Config represents the labeler service configuration.
|
||||
// It reuses the appview config YAML structure, reading from the "labeler" section.
|
||||
// Config represents the labeler service configuration. It is fully self-contained:
|
||||
// no fields are inherited from or shared with the appview config.
|
||||
type Config struct {
|
||||
Version string `yaml:"version" comment:"Configuration format version."`
|
||||
LogLevel string `yaml:"log_level" comment:"Log level: debug, info, warn, error."`
|
||||
Labeler LabelerConfig `yaml:"labeler" comment:"Labeler service settings."`
|
||||
Server AppviewServerConfig `yaml:"server" comment:"AppView server settings (shared config)."`
|
||||
LogShipper config.LogShipperConfig `yaml:"log_shipper" comment:"Remote log shipping settings."`
|
||||
}
|
||||
|
||||
@@ -32,10 +30,14 @@ type LabelerConfig struct {
|
||||
// Listen address for the labeler HTTP server.
|
||||
Addr string `yaml:"addr" comment:"Listen address for labeler (e.g., :5002)."`
|
||||
|
||||
// PublicURL is the externally reachable URL of the labeler. When empty the URL is
|
||||
// derived from server.base_url by prefixing "labeler." (so https://atcr.io →
|
||||
// https://labeler.atcr.io). Set explicitly for IP-based dev environments.
|
||||
PublicURL string `yaml:"public_url" comment:"Externally reachable labeler URL. Empty = derive from server.base_url."`
|
||||
// PublicURL is the externally reachable URL of the labeler. Required.
|
||||
PublicURL string `yaml:"public_url" comment:"Externally reachable labeler URL (required, e.g. https://labeler.example.com)."`
|
||||
|
||||
// ClientName is the OAuth client display name shown to PDS users on consent screens.
|
||||
ClientName string `yaml:"client_name" comment:"OAuth client display name (e.g., \"ATCR Labeler\")."`
|
||||
|
||||
// ClientShortName is a shorter brand label used in UI copy.
|
||||
ClientShortName string `yaml:"client_short_name" comment:"Short brand label used in UI copy (e.g., \"ATCR\")."`
|
||||
|
||||
// DID of the labeler admin. Only this DID can log into the admin panel.
|
||||
OwnerDID string `yaml:"owner_did" comment:"DID of the labeler admin. Only this DID can log into the admin panel."`
|
||||
@@ -69,27 +71,9 @@ type LabelerConfig struct {
|
||||
LibsqlSyncInterval time.Duration `yaml:"libsql_sync_interval" comment:"Embedded-replica pull interval (e.g. 30s). 0 = manual sync only."`
|
||||
}
|
||||
|
||||
// AppviewServerConfig is a subset of the appview ServerConfig that the labeler needs.
|
||||
type AppviewServerConfig struct {
|
||||
BaseURL string `yaml:"base_url"`
|
||||
ClientName string `yaml:"client_name"`
|
||||
ClientShortName string `yaml:"client_short_name"`
|
||||
TestMode bool `yaml:"test_mode"`
|
||||
}
|
||||
|
||||
// PublicURL returns the labeler's externally reachable URL. When labeler.public_url
|
||||
// is set explicitly it wins; otherwise it's derived from server.base_url by prefixing
|
||||
// "labeler." (so https://atcr.io → https://labeler.atcr.io).
|
||||
// PublicURL returns the labeler's externally reachable URL.
|
||||
func (c *Config) PublicURL() string {
|
||||
if c.Labeler.PublicURL != "" {
|
||||
return c.Labeler.PublicURL
|
||||
}
|
||||
u, err := url.Parse(c.Server.BaseURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
u.Host = "labeler." + u.Host
|
||||
return u.String()
|
||||
return c.Labeler.PublicURL
|
||||
}
|
||||
|
||||
// DBPath returns the path to the SQLite database file inside the data dir.
|
||||
@@ -121,6 +105,8 @@ func setDefaults(v *viper.Viper) {
|
||||
v.SetDefault("labeler.enabled", false)
|
||||
v.SetDefault("labeler.addr", ":5002")
|
||||
v.SetDefault("labeler.public_url", "")
|
||||
v.SetDefault("labeler.client_name", "ATCR Labeler")
|
||||
v.SetDefault("labeler.client_short_name", "ATCR")
|
||||
v.SetDefault("labeler.owner_did", "")
|
||||
v.SetDefault("labeler.data_dir", "/var/lib/atcr-labeler")
|
||||
v.SetDefault("labeler.did_method", "plc")
|
||||
@@ -131,15 +117,9 @@ func setDefaults(v *viper.Viper) {
|
||||
v.SetDefault("labeler.libsql_sync_url", "")
|
||||
v.SetDefault("labeler.libsql_auth_token", "")
|
||||
v.SetDefault("labeler.libsql_sync_interval", 0)
|
||||
|
||||
// Server defaults (read from shared appview config)
|
||||
v.SetDefault("server.base_url", "")
|
||||
v.SetDefault("server.client_name", "AT Container Registry")
|
||||
v.SetDefault("server.client_short_name", "ATCR")
|
||||
v.SetDefault("server.test_mode", false)
|
||||
}
|
||||
|
||||
// LoadConfig loads the labeler configuration from the appview config YAML.
|
||||
// LoadConfig loads the labeler configuration from a YAML file.
|
||||
func LoadConfig(yamlPath string) (*Config, error) {
|
||||
v := config.NewViper("LABELER", yamlPath)
|
||||
setDefaults(v)
|
||||
@@ -149,24 +129,9 @@ func LoadConfig(yamlPath string) (*Config, error) {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
// Also try ATCR_ prefix for shared server config
|
||||
atcrV := config.NewViper("ATCR", yamlPath)
|
||||
if baseURL := atcrV.GetString("server.base_url"); baseURL != "" && cfg.Server.BaseURL == "" {
|
||||
cfg.Server.BaseURL = baseURL
|
||||
}
|
||||
if clientName := atcrV.GetString("server.client_name"); clientName != "" && cfg.Server.ClientName == "" {
|
||||
cfg.Server.ClientName = clientName
|
||||
}
|
||||
if clientShortName := atcrV.GetString("server.client_short_name"); clientShortName != "" && cfg.Server.ClientShortName == "" {
|
||||
cfg.Server.ClientShortName = clientShortName
|
||||
}
|
||||
if atcrV.GetBool("server.test_mode") {
|
||||
cfg.Server.TestMode = true
|
||||
}
|
||||
|
||||
// Validation
|
||||
if cfg.Server.BaseURL == "" {
|
||||
return nil, fmt.Errorf("server.base_url is required")
|
||||
if cfg.Labeler.PublicURL == "" {
|
||||
return nil, fmt.Errorf("labeler.public_url is required")
|
||||
}
|
||||
if cfg.Labeler.OwnerDID == "" {
|
||||
return nil, fmt.Errorf("labeler.owner_did is required")
|
||||
@@ -188,14 +153,12 @@ func ExampleYAML() ([]byte, error) {
|
||||
cfg := &Config{
|
||||
Version: "0.1",
|
||||
LogLevel: "info",
|
||||
Server: AppviewServerConfig{
|
||||
BaseURL: "https://atcr.io",
|
||||
ClientName: "AT Container Registry",
|
||||
ClientShortName: "ATCR",
|
||||
},
|
||||
Labeler: LabelerConfig{
|
||||
Enabled: true,
|
||||
Addr: ":5002",
|
||||
PublicURL: "https://labeler.example.com",
|
||||
ClientName: "ATCR Labeler",
|
||||
ClientShortName: "ATCR",
|
||||
OwnerDID: "did:plc:your-did-here",
|
||||
DataDir: "/var/lib/atcr-labeler",
|
||||
DIDMethod: "plc",
|
||||
|
||||
@@ -3,25 +3,10 @@ package labeler
|
||||
import "testing"
|
||||
|
||||
func TestConfig_PublicURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseURL string
|
||||
want string
|
||||
}{
|
||||
{"standard", "https://atcr.io", "https://labeler.atcr.io"},
|
||||
{"with port", "https://atcr.io:8080", "https://labeler.atcr.io:8080"},
|
||||
{"localhost", "http://localhost:5000", "http://labeler.localhost:5000"},
|
||||
cfg := &Config{
|
||||
Labeler: LabelerConfig{PublicURL: "https://labeler.atcr.io"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Server: AppviewServerConfig{BaseURL: tt.baseURL},
|
||||
}
|
||||
got := cfg.PublicURL()
|
||||
if got != tt.want {
|
||||
t.Errorf("PublicURL() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
if got := cfg.PublicURL(); got != "https://labeler.atcr.io" {
|
||||
t.Errorf("PublicURL() = %q, want %q", got, "https://labeler.atcr.io")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ button{padding:10px 20px;cursor:pointer}</style>
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
</body></html>`,
|
||||
s.config.Server.ClientShortName,
|
||||
s.config.Server.ClientShortName,
|
||||
s.config.Labeler.ClientShortName,
|
||||
s.config.Labeler.ClientShortName,
|
||||
func() string {
|
||||
if errorMsg != "" {
|
||||
return fmt.Sprintf(`<div class="error">%s</div>`, template.HTMLEscapeString(errorMsg))
|
||||
|
||||
@@ -58,7 +58,7 @@ func (s *Server) handleClientMetadata(w http.ResponseWriter, r *http.Request) {
|
||||
publicURL := s.config.PublicURL()
|
||||
metadata := map[string]any{
|
||||
"client_id": publicURL + "/oauth-client-metadata.json",
|
||||
"client_name": fmt.Sprintf("%s Labeler", s.config.Server.ClientShortName),
|
||||
"client_name": s.config.Labeler.ClientName,
|
||||
"client_uri": publicURL,
|
||||
"redirect_uris": []string{publicURL + "/auth/oauth/callback"},
|
||||
"scope": "atproto",
|
||||
|
||||
@@ -462,8 +462,8 @@ code{background:#f4f4f5;padding:1px 4px;border-radius:3px}
|
||||
<a href="/auth/logout">Logout</a>
|
||||
</nav>
|
||||
<h2>Active Takedowns (%d)</h2>`,
|
||||
s.config.Server.ClientShortName,
|
||||
s.config.Server.ClientShortName,
|
||||
s.config.Labeler.ClientShortName,
|
||||
s.config.Labeler.ClientShortName,
|
||||
activeTotal,
|
||||
)
|
||||
|
||||
@@ -636,7 +636,7 @@ nav{display:flex;gap:16px;margin-bottom:24px}
|
||||
<a href="/" class="nav-btn">Dashboard</a>
|
||||
<a href="/takedown" class="nav-btn">New Takedown</a>
|
||||
</nav>`,
|
||||
s.config.Server.ClientShortName,
|
||||
s.config.Labeler.ClientShortName,
|
||||
)
|
||||
|
||||
if msg != "" {
|
||||
|
||||
Reference in New Issue
Block a user