diff --git a/README.md b/README.md index 59658fc..040cf2f 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Invoke-WebRequest -Uri "https://github.com/catio-tech/kportforward/releases/late ``` - Without `--multi-env`, behavior is unchanged (single environment, current kubectl context). - Environments come from a **separate** config, `environments.yaml` (embedded default; override at `~/.config/kportforward/environments.yaml`). Each environment pins a `context` and lists its own services/ports. - - Ports encode the environment: each service keeps the same last three digits and differs only by the prefix — **dev → `50xxx`**, **prod → `70xxx`** (e.g. `extractor-config-service` is `50102` for dev, `70102` for prod). (`overwatch` is dev-only, on `50030`.) + - Ports encode the environment: each service keeps the same last three digits and differs only by the prefix — **dev → `50xxx`**, **prod → `60xxx`** (e.g. `extractor-config-service` is `50102` for dev, `60102` for prod). Prefixes stay ≤ 64 because TCP ports max out at 65535. (`overwatch` is dev-only, on `50030`.) - In `--multi-env` a port conflict is a **hard error** (no silent reassignment), so the local port always identifies the environment. ## ⚙️ Configuration diff --git a/internal/config/config.go b/internal/config/config.go index 1ae9708..0216e6c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -66,6 +66,9 @@ func LoadMultiEnvConfig() (*MultiEnvConfig, error) { if len(cfg.Environments) == 0 { return nil, fmt.Errorf("multi-env config has no environments") } + if err := validateMultiEnvConfig(cfg); err != nil { + return nil, err + } // Fall back to the single-env defaults for unset UI/monitoring settings. if cfg.MonitoringInterval == 0 { cfg.MonitoringInterval = time.Second @@ -79,6 +82,25 @@ func LoadMultiEnvConfig() (*MultiEnvConfig, error) { return cfg, nil } +// validateMultiEnvConfig rejects a multi-env config that cannot work: an +// environment without a context, or a service whose local port is outside the +// valid TCP range (1-65535). Catching this at load gives a clear error instead +// of a misleading "port already in use" when the OS later refuses to bind it. +func validateMultiEnvConfig(mec *MultiEnvConfig) error { + for _, env := range mec.Environments { + if env.Context == "" { + return fmt.Errorf("multi-env: environment %q has no context", env.Name) + } + for name, svc := range env.Services { + if svc.LocalPort < 1 || svc.LocalPort > 65535 { + return fmt.Errorf("multi-env: environment %q service %q localPort %d is outside the valid range 1-65535", + env.Name, name, svc.LocalPort) + } + } + } + return nil +} + // FlattenEnvironments collapses a MultiEnvConfig into a single Config whose // PortForwards contains every environment's services, each keyed as // "-" and pinned to that environment's kubectl context. This lets diff --git a/internal/config/environments.yaml b/internal/config/environments.yaml index aa8cae6..047576e 100644 --- a/internal/config/environments.yaml +++ b/internal/config/environments.yaml @@ -7,7 +7,8 @@ # Each environment pins a kubectl context and forwards its own service list on a # distinct port band, so both run at once with no switchover and no ambiguity. # For every service the last three digits (xxx) match across environments; only -# the prefix differs: dev -> 50xxx, prod -> 70xxx (prod = dev + 20000). +# the prefix differs: dev -> 50xxx, prod -> 60xxx (prod = dev + 10000). +# (TCP ports max out at 65535, so both prefixes stay <= 64.) # (overwatch is dev-only — its namespace is dev-specific — so it has no prod entry.) # # In --multi-env mode a port conflict is a hard error (no silent reassignment), @@ -153,7 +154,7 @@ environments: overwatch: target: service/overwatch-web targetPort: 80 - localPort: 50030 + localPort: 3030 namespace: overwatch-dev-us-a type: web - name: prod @@ -162,55 +163,55 @@ environments: arch-inventory: target: service/architecture-inventory targetPort: 80 - localPort: 70100 + localPort: 60100 namespace: catio-data-extraction type: rpc recommendations-mgnt: target: service/recommendations-mgnt targetPort: 50051 - localPort: 70106 + localPort: 60106 namespace: catio-data-extraction type: rpc process-monitor: target: service/process-monitor-v2 targetPort: 5001 - localPort: 70101 + localPort: 60101 namespace: catio-data-extraction type: rpc extractor-config-service: target: service/extractor-config-service-rpc targetPort: 50051 - localPort: 70102 + localPort: 60102 namespace: catio-data-extraction type: rpc extractor-trigger: target: service/extractor-trigger targetPort: 80 - localPort: 70027 + localPort: 60027 namespace: catio-data-extraction type: rest workflow-trigger: target: service/workflow-trigger targetPort: 80 - localPort: 70105 + localPort: 60105 namespace: catio-data-extraction type: rpc file-management-api: target: service/file-management-api targetPort: 80 - localPort: 70115 + localPort: 60115 namespace: catio-data-extraction type: rpc flyte-console: target: service/flyteconsole targetPort: 80 - localPort: 70088 + localPort: 60088 namespace: flyte type: web flyte-admin-rpc: target: service/flyteadmin targetPort: 81 - localPort: 70089 + localPort: 60089 namespace: flyte type: rpc swaggerPath: configuration/swagger @@ -218,72 +219,72 @@ environments: flyte-admin-web: target: service/flyteadmin targetPort: 80 - localPort: 70081 + localPort: 60081 namespace: flyte type: other archie-backend: target: service/archie-backend targetPort: 50051 - localPort: 70108 + localPort: 60108 namespace: catio-data-extraction type: rpc llm-router: target: service/llm-router targetPort: 80 - localPort: 70107 + localPort: 60107 namespace: catio-data-extraction type: rpc question-service: target: service/question-service targetPort: 80 - localPort: 70110 + localPort: 60110 namespace: catio-data-extraction type: rpc views-service: target: service/views-service targetPort: 50051 - localPort: 70111 + localPort: 60111 namespace: catio-data-extraction type: rpc catiopipe: target: service/catiopipe-grpc targetPort: 50051 - localPort: 70112 + localPort: 60112 namespace: catio-data-extraction type: rpc eval-service: target: service/eval-service targetPort: 50051 - localPort: 70113 + localPort: 60113 namespace: catio-data-extraction type: rpc eval-dashboard: target: service/eval-dashboard targetPort: 3000 - localPort: 70114 + localPort: 60114 namespace: catio-data-extraction type: web cost-api: target: service/cost-api targetPort: 50402 - localPort: 70402 + localPort: 60402 namespace: catio-data-extraction type: rpc workspace-management-api: target: service/workspace-management-api targetPort: 80 - localPort: 70116 + localPort: 60116 namespace: catio-data-extraction type: rpc akb-search-api: target: service/akb-search-api targetPort: 80 - localPort: 70117 + localPort: 60117 namespace: catio-data-extraction type: rpc architecture-knowledge-base-ui: target: service/architecture-knowledge-base-ui targetPort: 3000 - localPort: 70118 + localPort: 60118 namespace: catio-data-extraction type: web diff --git a/internal/config/environments_test.go b/internal/config/environments_test.go index 8f644f8..cb08fd6 100644 --- a/internal/config/environments_test.go +++ b/internal/config/environments_test.go @@ -46,8 +46,43 @@ func TestLoadMultiEnvConfigEmbedded(t *testing.T) { t.Fatalf("dev extractor-config-service expected in 50xxx, got %+v (ok=%v)", ds.LocalPort, ok) } ps, ok := prod.Services["extractor-config-service"] - if !ok || ps.LocalPort < 70000 || ps.LocalPort >= 80000 { - t.Fatalf("prod extractor-config-service expected in 70xxx, got %+v (ok=%v)", ps.LocalPort, ok) + if !ok || ps.LocalPort < 60000 || ps.LocalPort >= 70000 { + t.Fatalf("prod extractor-config-service expected in 60xxx, got %+v (ok=%v)", ps.LocalPort, ok) + } + + // Every configured port must be a valid TCP port (<= 65535); 70xxx would not be. + for _, e := range mec.Environments { + for name, s := range e.Services { + if s.LocalPort < 1 || s.LocalPort > 65535 { + t.Errorf("%s/%s localPort %d is outside the valid TCP range", e.Name, name, s.LocalPort) + } + } + } +} + +// TestValidateMultiEnvConfigRejectsBadPort ensures an out-of-range port is +// rejected at load with a clear error (rather than surfacing later as a +// misleading "port already in use"). +func TestValidateMultiEnvConfigRejectsBadPort(t *testing.T) { + ok := &MultiEnvConfig{Environments: []Environment{ + {Name: "prod", Context: "ctx", Services: map[string]Service{"a": {LocalPort: 60102}}}, + }} + if err := validateMultiEnvConfig(ok); err != nil { + t.Fatalf("valid config rejected: %v", err) + } + + bad := &MultiEnvConfig{Environments: []Environment{ + {Name: "prod", Context: "ctx", Services: map[string]Service{"a": {LocalPort: 70102}}}, + }} + if err := validateMultiEnvConfig(bad); err == nil { + t.Fatal("expected out-of-range port 70102 to be rejected, got nil") + } + + noCtx := &MultiEnvConfig{Environments: []Environment{ + {Name: "prod", Context: "", Services: map[string]Service{"a": {LocalPort: 60102}}}, + }} + if err := validateMultiEnvConfig(noCtx); err == nil { + t.Fatal("expected environment without context to be rejected, got nil") } } @@ -73,8 +108,8 @@ func TestMultiEnvPortsMatchAcrossEnvironments(t *testing.T) { if ds.LocalPort/1000 != 50 { t.Errorf("dev %s port %d is not in the 50xxx band", name, ds.LocalPort) } - if ps.LocalPort/1000 != 70 { - t.Errorf("prod %s port %d is not in the 70xxx band", name, ps.LocalPort) + if ps.LocalPort/1000 != 60 { + t.Errorf("prod %s port %d is not in the 60xxx band", name, ps.LocalPort) } if ds.LocalPort%1000 != ps.LocalPort%1000 { t.Errorf("%s: dev(%d) and prod(%d) must share the last three digits", name, ds.LocalPort, ps.LocalPort)