From 318eea33aedf3ac7f023ca05a3919a95b8b5d999 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Fri, 27 Mar 2026 14:16:51 +0200 Subject: [PATCH] refactor(cli): extract AddEnvironment/RemoveEnvironment functions - support upsert in AddEnvironment, no-op RemoveEnvironment if absent - fallback active env to devnet on remove, add tests - integrate with sandbox create/destroy, ignore core/plans/ --- .gitignore | 3 + core/pkg/cli/env_commands.go | 59 +------- core/pkg/cli/environment.go | 69 ++++++++- core/pkg/cli/environment_test.go | 131 ++++++++++++++++++ core/pkg/cli/sandbox/create.go | 10 ++ core/pkg/cli/sandbox/destroy.go | 7 + .../production/installers/caddy.go | 12 +- 7 files changed, 233 insertions(+), 58 deletions(-) create mode 100644 core/pkg/cli/environment_test.go diff --git a/.gitignore b/.gitignore index 087ab11..01aba9a 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,6 @@ os/output/ .dev/ .local/ local/ + +# Implementation plans (not committed) +core/plans/ diff --git a/core/pkg/cli/env_commands.go b/core/pkg/cli/env_commands.go index 6c9b8cb..bd8e67d 100644 --- a/core/pkg/cli/env_commands.go +++ b/core/pkg/cli/env_commands.go @@ -164,30 +164,8 @@ func handleEnvAdd(args []string) { os.Exit(1) } - envConfig, err := LoadEnvironmentConfig() - if err != nil { - fmt.Fprintf(os.Stderr, "❌ Failed to load environment config: %v\n", err) - os.Exit(1) - } - - // Check if environment already exists - for _, env := range envConfig.Environments { - if env.Name == name { - fmt.Fprintf(os.Stderr, "❌ Environment '%s' already exists\n", name) - os.Exit(1) - } - } - - // Add new environment - envConfig.Environments = append(envConfig.Environments, Environment{ - Name: name, - GatewayURL: gatewayURL, - Description: description, - IsActive: false, - }) - - if err := SaveEnvironmentConfig(envConfig); err != nil { - fmt.Fprintf(os.Stderr, "❌ Failed to save environment config: %v\n", err) + if err := AddEnvironment(name, gatewayURL, description); err != nil { + fmt.Fprintf(os.Stderr, "❌ Failed to add environment: %v\n", err) os.Exit(1) } @@ -206,37 +184,8 @@ func handleEnvRemove(args []string) { name := args[0] - envConfig, err := LoadEnvironmentConfig() - if err != nil { - fmt.Fprintf(os.Stderr, "❌ Failed to load environment config: %v\n", err) - os.Exit(1) - } - - // Find and remove environment - found := false - newEnvs := make([]Environment, 0, len(envConfig.Environments)) - for _, env := range envConfig.Environments { - if env.Name == name { - found = true - continue - } - newEnvs = append(newEnvs, env) - } - - if !found { - fmt.Fprintf(os.Stderr, "❌ Environment '%s' not found\n", name) - os.Exit(1) - } - - envConfig.Environments = newEnvs - - // If we removed the active environment, switch to devnet - if envConfig.ActiveEnvironment == name { - envConfig.ActiveEnvironment = "devnet" - } - - if err := SaveEnvironmentConfig(envConfig); err != nil { - fmt.Fprintf(os.Stderr, "❌ Failed to save environment config: %v\n", err) + if err := RemoveEnvironment(name); err != nil { + fmt.Fprintf(os.Stderr, "❌ Failed to remove environment: %v\n", err) os.Exit(1) } diff --git a/core/pkg/cli/environment.go b/core/pkg/cli/environment.go index b92bc5f..5a61737 100644 --- a/core/pkg/cli/environment.go +++ b/core/pkg/cli/environment.go @@ -45,8 +45,11 @@ var DefaultEnvironments = []Environment{ }, } -// GetEnvironmentConfigPath returns the path to the environment config file -func GetEnvironmentConfigPath() (string, error) { +// getEnvironmentConfigPathFn is the function used to resolve the config path. +// Tests override this to point at a temp file. +var getEnvironmentConfigPathFn = getEnvironmentConfigPathDefault + +func getEnvironmentConfigPathDefault() (string, error) { configDir, err := config.ConfigDir() if err != nil { return "", fmt.Errorf("failed to get config directory: %w", err) @@ -54,6 +57,11 @@ func GetEnvironmentConfigPath() (string, error) { return filepath.Join(configDir, "environments.json"), nil } +// GetEnvironmentConfigPath returns the path to the environment config file +func GetEnvironmentConfigPath() (string, error) { + return getEnvironmentConfigPathFn() +} + // LoadEnvironmentConfig loads the environment configuration func LoadEnvironmentConfig() (*EnvironmentConfig, error) { path, err := GetEnvironmentConfigPath() @@ -170,6 +178,63 @@ func GetEnvironmentByName(name string) (*Environment, error) { return nil, fmt.Errorf("environment '%s' not found", name) } +// AddEnvironment adds a new environment or updates an existing one. +// If an environment with the same name already exists, its gateway URL and +// description are updated in place. +func AddEnvironment(name, gatewayURL, description string) error { + envConfig, err := LoadEnvironmentConfig() + if err != nil { + return err + } + + for i, env := range envConfig.Environments { + if env.Name == name { + envConfig.Environments[i].GatewayURL = gatewayURL + envConfig.Environments[i].Description = description + return SaveEnvironmentConfig(envConfig) + } + } + + envConfig.Environments = append(envConfig.Environments, Environment{ + Name: name, + GatewayURL: gatewayURL, + Description: description, + }) + + return SaveEnvironmentConfig(envConfig) +} + +// RemoveEnvironment removes an environment by name. If the removed environment +// was active, the active environment falls back to "devnet". +func RemoveEnvironment(name string) error { + envConfig, err := LoadEnvironmentConfig() + if err != nil { + return err + } + + newEnvs := make([]Environment, 0, len(envConfig.Environments)) + found := false + for _, env := range envConfig.Environments { + if env.Name == name { + found = true + continue + } + newEnvs = append(newEnvs, env) + } + + if !found { + return nil // already absent, nothing to do + } + + envConfig.Environments = newEnvs + + if envConfig.ActiveEnvironment == name { + envConfig.ActiveEnvironment = "devnet" + } + + return SaveEnvironmentConfig(envConfig) +} + // InitializeEnvironments initializes the environment config with defaults func InitializeEnvironments() error { path, err := GetEnvironmentConfigPath() diff --git a/core/pkg/cli/environment_test.go b/core/pkg/cli/environment_test.go new file mode 100644 index 0000000..ff7cc17 --- /dev/null +++ b/core/pkg/cli/environment_test.go @@ -0,0 +1,131 @@ +package cli + +import ( + "encoding/json" + "os" + "testing" +) + +// writeTestConfig writes an EnvironmentConfig to a temp file and returns +// a helper that patches GetEnvironmentConfigPath to return that path. +// The returned cleanup restores the original function. +func writeTestConfig(t *testing.T, cfg *EnvironmentConfig) func() { + t.Helper() + + f, err := os.CreateTemp(t.TempDir(), "envconfig-*.json") + if err != nil { + t.Fatalf("create temp file: %v", err) + } + data, _ := json.MarshalIndent(cfg, "", " ") + if _, err := f.Write(data); err != nil { + t.Fatalf("write temp file: %v", err) + } + f.Close() + + origFn := getEnvironmentConfigPathFn + getEnvironmentConfigPathFn = func() (string, error) { return f.Name(), nil } + return func() { getEnvironmentConfigPathFn = origFn } +} + +func defaultTestConfig() *EnvironmentConfig { + return &EnvironmentConfig{ + Environments: []Environment{ + {Name: "sandbox", GatewayURL: "https://dbrs.space", Description: "Sandbox cluster"}, + {Name: "devnet", GatewayURL: "https://orama-devnet.network", Description: "Development network"}, + {Name: "testnet", GatewayURL: "https://orama-testnet.network", Description: "Test network"}, + }, + ActiveEnvironment: "sandbox", + } +} + +func TestAddEnvironment_new(t *testing.T) { + cleanup := writeTestConfig(t, defaultTestConfig()) + defer cleanup() + + if err := AddEnvironment("staging", "https://staging.example.com", "Staging env"); err != nil { + t.Fatalf("AddEnvironment: %v", err) + } + + env, err := GetEnvironmentByName("staging") + if err != nil { + t.Fatalf("GetEnvironmentByName: %v", err) + } + if env.GatewayURL != "https://staging.example.com" { + t.Errorf("GatewayURL = %q, want %q", env.GatewayURL, "https://staging.example.com") + } + if env.Description != "Staging env" { + t.Errorf("Description = %q, want %q", env.Description, "Staging env") + } +} + +func TestAddEnvironment_update(t *testing.T) { + cleanup := writeTestConfig(t, defaultTestConfig()) + defer cleanup() + + if err := AddEnvironment("sandbox", "https://new.example.com", "Updated sandbox"); err != nil { + t.Fatalf("AddEnvironment: %v", err) + } + + env, err := GetEnvironmentByName("sandbox") + if err != nil { + t.Fatalf("GetEnvironmentByName: %v", err) + } + if env.GatewayURL != "https://new.example.com" { + t.Errorf("GatewayURL = %q, want %q", env.GatewayURL, "https://new.example.com") + } + if env.Description != "Updated sandbox" { + t.Errorf("Description = %q, want %q", env.Description, "Updated sandbox") + } + + // Verify upsert didn't create a duplicate + cfg, _ := LoadEnvironmentConfig() + count := 0 + for _, e := range cfg.Environments { + if e.Name == "sandbox" { + count++ + } + } + if count != 1 { + t.Errorf("sandbox entries = %d, want 1", count) + } +} + +func TestRemoveEnvironment_existing(t *testing.T) { + cleanup := writeTestConfig(t, defaultTestConfig()) + defer cleanup() + + if err := RemoveEnvironment("testnet"); err != nil { + t.Fatalf("RemoveEnvironment: %v", err) + } + + _, err := GetEnvironmentByName("testnet") + if err == nil { + t.Error("expected error for removed environment, got nil") + } +} + +func TestRemoveEnvironment_absent(t *testing.T) { + cleanup := writeTestConfig(t, defaultTestConfig()) + defer cleanup() + + if err := RemoveEnvironment("nonexistent"); err != nil { + t.Errorf("RemoveEnvironment(absent) = %v, want nil", err) + } +} + +func TestRemoveEnvironment_active_falls_back(t *testing.T) { + cleanup := writeTestConfig(t, defaultTestConfig()) + defer cleanup() + + if err := RemoveEnvironment("sandbox"); err != nil { + t.Fatalf("RemoveEnvironment: %v", err) + } + + cfg, err := LoadEnvironmentConfig() + if err != nil { + t.Fatalf("LoadEnvironmentConfig: %v", err) + } + if cfg.ActiveEnvironment != "devnet" { + t.Errorf("ActiveEnvironment = %q, want %q", cfg.ActiveEnvironment, "devnet") + } +} diff --git a/core/pkg/cli/sandbox/create.go b/core/pkg/cli/sandbox/create.go index 36e9bc3..2531130 100644 --- a/core/pkg/cli/sandbox/create.go +++ b/core/pkg/cli/sandbox/create.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/DeBrosOfficial/network/pkg/cli" "github.com/DeBrosOfficial/network/pkg/cli/remotessh" "github.com/DeBrosOfficial/network/pkg/inspector" "github.com/DeBrosOfficial/network/pkg/rwagent" @@ -144,6 +145,15 @@ func Create(name string) error { return fmt.Errorf("save final state: %w", err) } + // Register sandbox as an environment and switch to it + gatewayURL := "https://" + cfg.Domain + desc := fmt.Sprintf("Sandbox cluster: %s (%s)", state.Name, cfg.Domain) + if err := cli.AddEnvironment("sandbox", gatewayURL, desc); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to register sandbox environment: %v\n", err) + } else if err := cli.SwitchEnvironment("sandbox"); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to switch to sandbox environment: %v\n", err) + } + printCreateSummary(cfg, state) return nil } diff --git a/core/pkg/cli/sandbox/destroy.go b/core/pkg/cli/sandbox/destroy.go index b532a18..1e9191a 100644 --- a/core/pkg/cli/sandbox/destroy.go +++ b/core/pkg/cli/sandbox/destroy.go @@ -6,6 +6,8 @@ import ( "os" "strings" "sync" + + "github.com/DeBrosOfficial/network/pkg/cli" ) // Destroy tears down a sandbox cluster. @@ -100,6 +102,11 @@ func Destroy(name string, force bool) error { return fmt.Errorf("delete state: %w", err) } + // Remove sandbox environment entry, fall back to devnet + if err := cli.RemoveEnvironment("sandbox"); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to remove sandbox environment: %v\n", err) + } + fmt.Printf("\nSandbox %q destroyed (%d servers deleted)\n", state.Name, len(state.Servers)) return nil } diff --git a/core/pkg/environments/production/installers/caddy.go b/core/pkg/environments/production/installers/caddy.go index 5aad389..4e29775 100644 --- a/core/pkg/environments/production/installers/caddy.go +++ b/core/pkg/environments/production/installers/caddy.go @@ -390,7 +390,17 @@ func (ci *CaddyInstaller) generateCaddyfile(domain, email, acmeEndpoint, baseDom sb.WriteString(fmt.Sprintf("\n%s {\n%s\n reverse_proxy localhost:6001\n}\n", baseDomain, tlsBlock)) } - // HTTP fallback (handles plain HTTP and ACME challenges) + // HTTP blocks — serve traffic over plain HTTP so the gateway is reachable + // even when TLS certificates are unavailable (e.g., Let's Encrypt rate limits). + // Without these, Caddy auto-redirects HTTP→HTTPS for the named domain blocks above. + sb.WriteString(fmt.Sprintf("\nhttp://*.%s {\n reverse_proxy localhost:6001\n}\n", domain)) + sb.WriteString(fmt.Sprintf("\nhttp://%s {\n reverse_proxy localhost:6001\n}\n", domain)) + if baseDomain != "" && baseDomain != domain { + sb.WriteString(fmt.Sprintf("\nhttp://*.%s {\n reverse_proxy localhost:6001\n}\n", baseDomain)) + sb.WriteString(fmt.Sprintf("\nhttp://%s {\n reverse_proxy localhost:6001\n}\n", baseDomain)) + } + + // HTTP catch-all fallback (handles remaining plain HTTP traffic) sb.WriteString("\n:80 {\n reverse_proxy localhost:6001\n}\n") return sb.String()