Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
// "<service>-<env>" and pinned to that environment's kubectl context. This lets
Expand Down
47 changes: 24 additions & 23 deletions internal/config/environments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand All @@ -162,128 +163,128 @@ 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
apiPath: api
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
43 changes: 39 additions & 4 deletions internal/config/environments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Expand All @@ -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)
Expand Down
Loading