From 9282fe64eef481abf61656273187e8ca03abe5b5 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Sun, 1 Feb 2026 12:01:31 +0200 Subject: [PATCH] Deployement updates --- e2e/cluster/namespace_isolation_test.go | 52 +++++++++---------- pkg/cli/utils/systemd.go | 46 +++++++++++++++- pkg/environments/production/installers.go | 7 ++- .../production/installers/anyone_relay.go | 18 ++++++- .../production/installers/coredns.go | 40 ++++++++++++++ .../production/installers/gateway.go | 19 ++++--- pkg/environments/production/orchestrator.go | 4 +- 7 files changed, 147 insertions(+), 39 deletions(-) diff --git a/e2e/cluster/namespace_isolation_test.go b/e2e/cluster/namespace_isolation_test.go index 5f7395b..2d7972e 100644 --- a/e2e/cluster/namespace_isolation_test.go +++ b/e2e/cluster/namespace_isolation_test.go @@ -17,14 +17,34 @@ import ( "github.com/stretchr/testify/require" ) -func TestNamespaceIsolation_Deployments(t *testing.T) { - // Setup two test environments with different namespaces +// TestNamespaceIsolation creates two namespaces once and runs all isolation +// subtests against them. This keeps namespace usage to 2 regardless of how +// many isolation scenarios we test. +func TestNamespaceIsolation(t *testing.T) { envA, err := e2e.LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) require.NoError(t, err, "Failed to create namespace A environment") envB, err := e2e.LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix())) require.NoError(t, err, "Failed to create namespace B environment") + t.Run("Deployments", func(t *testing.T) { + testNamespaceIsolationDeployments(t, envA, envB) + }) + + t.Run("SQLiteDatabases", func(t *testing.T) { + testNamespaceIsolationSQLiteDatabases(t, envA, envB) + }) + + t.Run("IPFSContent", func(t *testing.T) { + testNamespaceIsolationIPFSContent(t, envA, envB) + }) + + t.Run("OlricCache", func(t *testing.T) { + testNamespaceIsolationOlricCache(t, envA, envB) + }) +} + +func testNamespaceIsolationDeployments(t *testing.T, envA, envB *e2e.E2ETestEnv) { tarballPath := filepath.Join("../../testdata/apps/react-app") // Create deployment in namespace-a @@ -112,13 +132,7 @@ func TestNamespaceIsolation_Deployments(t *testing.T) { }) } -func TestNamespaceIsolation_SQLiteDatabases(t *testing.T) { - envA, err := e2e.LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) - require.NoError(t, err, "Should create test environment for namespace-a") - - envB, err := e2e.LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix())) - require.NoError(t, err, "Should create test environment for namespace-b") - +func testNamespaceIsolationSQLiteDatabases(t *testing.T, envA, envB *e2e.E2ETestEnv) { // Create database in namespace-a dbNameA := "users-db-a" e2e.CreateSQLiteDB(t, envA, dbNameA) @@ -201,13 +215,7 @@ func TestNamespaceIsolation_SQLiteDatabases(t *testing.T) { }) } -func TestNamespaceIsolation_IPFSContent(t *testing.T) { - envA, err := e2e.LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) - require.NoError(t, err, "Should create test environment for namespace-a") - - envB, err := e2e.LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix())) - require.NoError(t, err, "Should create test environment for namespace-b") - +func testNamespaceIsolationIPFSContent(t *testing.T, envA, envB *e2e.E2ETestEnv) { // Upload file in namespace-a cidA := e2e.UploadTestFile(t, envA, "test-file-a.txt", "Content from namespace A") defer func() { @@ -217,8 +225,6 @@ func TestNamespaceIsolation_IPFSContent(t *testing.T) { }() t.Run("Namespace-B cannot GET Namespace-A IPFS content", func(t *testing.T) { - // This tests application-level access control - // IPFS content is globally accessible by CID, but our handlers should enforce namespace req, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/storage/get/"+cidA, nil) req.Header.Set("Authorization", "Bearer "+envB.APIKey) @@ -226,7 +232,6 @@ func TestNamespaceIsolation_IPFSContent(t *testing.T) { require.NoError(t, err, "Should execute request") defer resp.Body.Close() - // Should return 403 or 404 (namespace doesn't own this CID) assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode, "Should block cross-namespace IPFS GET") @@ -273,13 +278,7 @@ func TestNamespaceIsolation_IPFSContent(t *testing.T) { }) } -func TestNamespaceIsolation_OlricCache(t *testing.T) { - envA, err := e2e.LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) - require.NoError(t, err, "Should create test environment for namespace-a") - - envB, err := e2e.LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix())) - require.NoError(t, err, "Should create test environment for namespace-b") - +func testNamespaceIsolationOlricCache(t *testing.T, envA, envB *e2e.E2ETestEnv) { dmap := "test-cache" keyA := "user-session-123" valueA := `{"user_id": "alice", "token": "secret-token-a"}` @@ -342,7 +341,6 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) { require.NoError(t, err, "Should execute request") defer resp.Body.Close() - // Should return 404 or success (key doesn't exist in their namespace) assert.Contains(t, []int{http.StatusOK, http.StatusNotFound}, resp.StatusCode) // Verify key still exists for namespace-a diff --git a/pkg/cli/utils/systemd.go b/pkg/cli/utils/systemd.go index 268b406..0e7a8d7 100644 --- a/pkg/cli/utils/systemd.go +++ b/pkg/cli/utils/systemd.go @@ -201,18 +201,60 @@ func CollectPortsForServices(services []string, skipActive bool) ([]PortSpec, er return ports, nil } -// EnsurePortsAvailable checks if the specified ports are available +// EnsurePortsAvailable checks if the specified ports are available. +// If a port is in use, it identifies the process and gives actionable guidance. func EnsurePortsAvailable(action string, ports []PortSpec) error { + var conflicts []string for _, spec := range ports { ln, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", spec.Port)) if err != nil { if errors.Is(err, syscall.EADDRINUSE) || strings.Contains(err.Error(), "address already in use") { - return fmt.Errorf("%s cannot continue: %s (port %d) is already in use", action, spec.Name, spec.Port) + processInfo := identifyPortProcess(spec.Port) + conflicts = append(conflicts, fmt.Sprintf(" - %s (port %d): %s", spec.Name, spec.Port, processInfo)) + continue } return fmt.Errorf("%s cannot continue: failed to inspect %s (port %d): %w", action, spec.Name, spec.Port, err) } _ = ln.Close() } + if len(conflicts) > 0 { + msg := fmt.Sprintf("%s cannot continue: the following ports are already in use:\n%s\n\n", action, strings.Join(conflicts, "\n")) + msg += "Please stop the conflicting services before running this command.\n" + msg += "Common fixes:\n" + msg += " - Docker: sudo systemctl stop docker docker.socket\n" + msg += " - Old IPFS: sudo systemctl stop ipfs\n" + msg += " - systemd-resolved: already handled by installer (port 53)\n" + msg += " - Other services: sudo kill or sudo systemctl stop " + return fmt.Errorf(msg) + } return nil } +// identifyPortProcess uses ss/lsof to find what process is using a port +func identifyPortProcess(port int) string { + // Try ss first (available on most Linux) + out, err := exec.Command("ss", "-tlnp", fmt.Sprintf("sport = :%d", port)).CombinedOutput() + if err == nil { + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + for _, line := range lines { + if strings.Contains(line, "users:") { + // Extract process info from ss output like: users:(("docker-proxy",pid=2049,fd=4)) + if idx := strings.Index(line, "users:"); idx != -1 { + return strings.TrimSpace(line[idx:]) + } + } + } + } + + // Fallback: try lsof + out, err = exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN", "-n", "-P").CombinedOutput() + if err == nil { + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + if len(lines) > 1 { + return strings.TrimSpace(lines[1]) // first data line after header + } + } + + return "unknown process" +} + diff --git a/pkg/environments/production/installers.go b/pkg/environments/production/installers.go index e8cd8b1..f9b490e 100644 --- a/pkg/environments/production/installers.go +++ b/pkg/environments/production/installers.go @@ -1,6 +1,7 @@ package production import ( + "fmt" "io" "os/exec" @@ -117,8 +118,12 @@ func (bi *BinaryInstaller) InstallAnyoneClient() error { return bi.gateway.InstallAnyoneClient() } -// InstallCoreDNS builds and installs CoreDNS with the custom RQLite plugin +// InstallCoreDNS builds and installs CoreDNS with the custom RQLite plugin. +// Also disables systemd-resolved's stub listener so CoreDNS can bind to port 53. func (bi *BinaryInstaller) InstallCoreDNS() error { + if err := bi.coredns.DisableResolvedStubListener(); err != nil { + fmt.Fprintf(bi.logWriter, " ⚠️ Failed to disable systemd-resolved stub: %v\n", err) + } return bi.coredns.Install() } diff --git a/pkg/environments/production/installers/anyone_relay.go b/pkg/environments/production/installers/anyone_relay.go index b38ecda..e5ea722 100644 --- a/pkg/environments/production/installers/anyone_relay.go +++ b/pkg/environments/production/installers/anyone_relay.go @@ -171,8 +171,17 @@ func (ari *AnyoneRelayInstaller) Install() error { return fmt.Errorf("failed to add Anyone repository: %w", err) } - // Install the anon package - cmd := exec.Command("apt-get", "install", "-y", "anon") + // Pre-accept terms via debconf to avoid interactive prompt during apt install. + // The anon package preinst script checks "anon/terms" via debconf. + preseed := exec.Command("bash", "-c", `echo "anon anon/terms boolean true" | debconf-set-selections`) + if output, err := preseed.CombinedOutput(); err != nil { + fmt.Fprintf(ari.logWriter, " ⚠️ debconf preseed warning: %v (%s)\n", err, string(output)) + } + + // Install the anon package non-interactively. + // --force-confold keeps existing config files if present (e.g. during migration). + cmd := exec.Command("apt-get", "install", "-y", "-o", "Dpkg::Options::=--force-confold", "anon") + cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive") if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to install anon package: %w\n%s", err, string(output)) } @@ -180,6 +189,11 @@ func (ari *AnyoneRelayInstaller) Install() error { // Clean up os.Remove(installScript) + // Stop and disable the default 'anon' systemd service that the apt package + // auto-enables. We use our own 'debros-anyone-relay' service instead. + exec.Command("systemctl", "stop", "anon").Run() + exec.Command("systemctl", "disable", "anon").Run() + fmt.Fprintf(ari.logWriter, " ✓ Anyone relay binary installed\n") // Install nyx for relay monitoring (connects to ControlPort 9051) diff --git a/pkg/environments/production/installers/coredns.go b/pkg/environments/production/installers/coredns.go index 8c92142..f92a2c7 100644 --- a/pkg/environments/production/installers/coredns.go +++ b/pkg/environments/production/installers/coredns.go @@ -54,6 +54,46 @@ func (ci *CoreDNSInstaller) IsInstalled() bool { } // Install builds and installs CoreDNS with the custom RQLite plugin +// DisableResolvedStubListener disables systemd-resolved's DNS stub listener +// so CoreDNS can bind to port 53. This is required on Ubuntu/Debian systems +// where systemd-resolved listens on 127.0.0.53:53 by default. +func (ci *CoreDNSInstaller) DisableResolvedStubListener() error { + // Check if systemd-resolved is running + if err := exec.Command("systemctl", "is-active", "--quiet", "systemd-resolved").Run(); err != nil { + return nil // Not running, nothing to do + } + + fmt.Fprintf(ci.logWriter, " Disabling systemd-resolved DNS stub listener (for CoreDNS)...\n") + + // Disable the stub listener + resolvedConf := "/etc/systemd/resolved.conf.d/no-stub.conf" + if err := os.MkdirAll("/etc/systemd/resolved.conf.d", 0755); err != nil { + return fmt.Errorf("failed to create resolved.conf.d: %w", err) + } + conf := "[Resolve]\nDNSStubListener=no\n" + if err := os.WriteFile(resolvedConf, []byte(conf), 0644); err != nil { + return fmt.Errorf("failed to write resolved config: %w", err) + } + + // Point resolv.conf to localhost (CoreDNS) and a fallback + resolvConf := "nameserver 127.0.0.1\nnameserver 8.8.8.8\n" + if err := os.Remove("/etc/resolv.conf"); err != nil && !os.IsNotExist(err) { + // It might be a symlink + fmt.Fprintf(ci.logWriter, " ⚠️ Could not remove /etc/resolv.conf: %v\n", err) + } + if err := os.WriteFile("/etc/resolv.conf", []byte(resolvConf), 0644); err != nil { + return fmt.Errorf("failed to write resolv.conf: %w", err) + } + + // Restart systemd-resolved + if output, err := exec.Command("systemctl", "restart", "systemd-resolved").CombinedOutput(); err != nil { + fmt.Fprintf(ci.logWriter, " ⚠️ Failed to restart systemd-resolved: %v (%s)\n", err, string(output)) + } + + fmt.Fprintf(ci.logWriter, " ✓ systemd-resolved stub listener disabled\n") + return nil +} + func (ci *CoreDNSInstaller) Install() error { if ci.IsInstalled() { fmt.Fprintf(ci.logWriter, " ✓ CoreDNS with RQLite plugin already installed\n") diff --git a/pkg/environments/production/installers/gateway.go b/pkg/environments/production/installers/gateway.go index 050d818..6322d9b 100644 --- a/pkg/environments/production/installers/gateway.go +++ b/pkg/environments/production/installers/gateway.go @@ -222,14 +222,21 @@ func (gi *GatewayInstaller) InstallDeBrosBinaries(branch string, oramaHome strin // InstallGo downloads and installs Go toolchain func (gi *GatewayInstaller) InstallGo() error { - if _, err := exec.LookPath("go"); err == nil { - fmt.Fprintf(gi.logWriter, " ✓ Go already installed\n") - return nil + requiredVersion := "1.22.5" + if goPath, err := exec.LookPath("go"); err == nil { + // Check version - upgrade if too old + out, _ := exec.Command(goPath, "version").Output() + if strings.Contains(string(out), "go"+requiredVersion) || strings.Contains(string(out), "go1.23") || strings.Contains(string(out), "go1.24") { + fmt.Fprintf(gi.logWriter, " ✓ Go already installed (%s)\n", strings.TrimSpace(string(out))) + return nil + } + fmt.Fprintf(gi.logWriter, " Upgrading Go (current: %s, need >= %s)...\n", strings.TrimSpace(string(out)), requiredVersion) + os.RemoveAll("/usr/local/go") + } else { + fmt.Fprintf(gi.logWriter, " Installing Go...\n") } - fmt.Fprintf(gi.logWriter, " Installing Go...\n") - - goTarball := fmt.Sprintf("go1.22.5.linux-%s.tar.gz", gi.arch) + goTarball := fmt.Sprintf("go%s.linux-%s.tar.gz", requiredVersion, gi.arch) goURL := fmt.Sprintf("https://go.dev/dl/%s", goTarball) // Download diff --git a/pkg/environments/production/orchestrator.go b/pkg/environments/production/orchestrator.go index b38399d..07347fe 100644 --- a/pkg/environments/production/orchestrator.go +++ b/pkg/environments/production/orchestrator.go @@ -640,9 +640,11 @@ func (ps *ProductionSetup) Phase5CreateSystemdServices(enableHTTPS bool) error { // Caddy service (for SSL/TLS with DNS-01 ACME challenges) if _, err := os.Stat("/usr/bin/caddy"); err == nil { // Create caddy user if it doesn't exist - exec.Command("useradd", "-r", "-s", "/sbin/nologin", "caddy").Run() + exec.Command("useradd", "-r", "-m", "-d", "/home/caddy", "-s", "/sbin/nologin", "caddy").Run() exec.Command("mkdir", "-p", "/var/lib/caddy").Run() exec.Command("chown", "caddy:caddy", "/var/lib/caddy").Run() + exec.Command("mkdir", "-p", "/home/caddy").Run() + exec.Command("chown", "caddy:caddy", "/home/caddy").Run() caddyUnit := ps.serviceGenerator.GenerateCaddyService() if err := ps.serviceController.WriteServiceUnit("caddy.service", caddyUnit); err != nil {