fix(cli): surface unresolved route targets
- commit
- 356789647a467bae7281f8c899c842fa4caf351b
- author
- brice · · 2026-04-14 18:10 UTC
- parent
- a0c8f05
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +106 -1
M internal/adapters/in/cli/controlplane_resolver.go +0 -1
| @@ -32,7 +32,6 @@ func resolveControlPlane(configPath string) (*controlPlaneHandle, error) { | |||
| 32 | 32 | } | |
| 33 | 33 | ||
| 34 | 34 | func resolveLocalControlPlane(configPath string) (*controlPlaneHandle, error) { | |
| 35 | - | ||
| 36 | 35 | kernel, err := app.NewKernel(configPath) | |
| 37 | 36 | if err != nil { | |
| 38 | 37 | return nil, fmt.Errorf("failed to initialize local control plane: %w", err) | |
| @@ -32,7 +32,6 @@ func resolveControlPlane(configPath string) (*controlPlaneHandle, error) { | |||||
| 32 | } | 32 | } | ||
| 33 | 33 | ||||
| 34 | func resolveLocalControlPlane(configPath string) (*controlPlaneHandle, error) { | 34 | func resolveLocalControlPlane(configPath string) (*controlPlaneHandle, error) { | ||
| 35 | - | ||||
| 36 | kernel, err := app.NewKernel(configPath) | 35 | kernel, err := app.NewKernel(configPath) | ||
| 37 | if err != nil { | 36 | if err != nil { | ||
| 38 | return nil, fmt.Errorf("failed to initialize local control plane: %w", err) | 37 | return nil, fmt.Errorf("failed to initialize local control plane: %w", err) | ||
M internal/adapters/in/cli/routes.go +18 -0
| @@ -269,6 +269,13 @@ func collectRoutesListSections(ctx context.Context, cfgPath string, deps routesL | |||
| 269 | 269 | section := loadRoutesListExplicitRemoteSection(ctx, deps, resolved) | |
| 270 | 270 | return []routeListSection{section}, nil | |
| 271 | 271 | } | |
| 272 | + | if target, ok := resolveRoutesExplicitTarget(); ok { | |
| 273 | + | return []routeListSection{{ | |
| 274 | + | Kind: "remote", | |
| 275 | + | Name: target, | |
| 276 | + | Error: unresolvedRoutesTargetError(target), | |
| 277 | + | }}, nil | |
| 278 | + | } | |
| 272 | 279 | ||
| 273 | 280 | return collectRoutesListAggregateSections(ctx, cfgPath, deps) | |
| 274 | 281 | } | |
| @@ -371,6 +378,13 @@ func collectRoutesStatusSections(ctx context.Context, cfgPath string, deps route | |||
| 371 | 378 | section := loadRoutesStatusExplicitRemoteSection(ctx, deps, resolved) | |
| 372 | 379 | return []routeStatusSection{section}, nil | |
| 373 | 380 | } | |
| 381 | + | if target, ok := resolveRoutesExplicitTarget(); ok { | |
| 382 | + | return []routeStatusSection{{ | |
| 383 | + | Kind: "remote", | |
| 384 | + | Name: target, | |
| 385 | + | Error: unresolvedRoutesTargetError(target), | |
| 386 | + | }}, nil | |
| 387 | + | } | |
| 374 | 388 | ||
| 375 | 389 | return collectRoutesStatusAggregateSections(ctx, cfgPath, deps) | |
| 376 | 390 | } | |
| @@ -585,6 +599,10 @@ func capitalizeKind(kind string) string { | |||
| 585 | 599 | return strings.ToUpper(kind[:1]) + kind[1:] | |
| 586 | 600 | } | |
| 587 | 601 | ||
| 602 | + | func unresolvedRoutesTargetError(target string) string { | |
| 603 | + | return fmt.Sprintf("could not resolve remote target %q", target) | |
| 604 | + | } | |
| 605 | + | ||
| 588 | 606 | func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool) { | |
| 589 | 607 | target, ok := resolveRoutesExplicitTarget() | |
| 590 | 608 | if !ok { | |
| @@ -269,6 +269,13 @@ func collectRoutesListSections(ctx context.Context, cfgPath string, deps routesL | |||||
| 269 | section := loadRoutesListExplicitRemoteSection(ctx, deps, resolved) | 269 | section := loadRoutesListExplicitRemoteSection(ctx, deps, resolved) | ||
| 270 | return []routeListSection{section}, nil | 270 | return []routeListSection{section}, nil | ||
| 271 | } | 271 | } | ||
| 272 | + | if target, ok := resolveRoutesExplicitTarget(); ok { | |||
| 273 | + | return []routeListSection{{ | |||
| 274 | + | Kind: "remote", | |||
| 275 | + | Name: target, | |||
| 276 | + | Error: unresolvedRoutesTargetError(target), | |||
| 277 | + | }}, nil | |||
| 278 | + | } | |||
| 272 | 279 | ||||
| 273 | return collectRoutesListAggregateSections(ctx, cfgPath, deps) | 280 | return collectRoutesListAggregateSections(ctx, cfgPath, deps) | ||
| 274 | } | 281 | } | ||
| @@ -371,6 +378,13 @@ func collectRoutesStatusSections(ctx context.Context, cfgPath string, deps route | |||||
| 371 | section := loadRoutesStatusExplicitRemoteSection(ctx, deps, resolved) | 378 | section := loadRoutesStatusExplicitRemoteSection(ctx, deps, resolved) | ||
| 372 | return []routeStatusSection{section}, nil | 379 | return []routeStatusSection{section}, nil | ||
| 373 | } | 380 | } | ||
| 381 | + | if target, ok := resolveRoutesExplicitTarget(); ok { | |||
| 382 | + | return []routeStatusSection{{ | |||
| 383 | + | Kind: "remote", | |||
| 384 | + | Name: target, | |||
| 385 | + | Error: unresolvedRoutesTargetError(target), | |||
| 386 | + | }}, nil | |||
| 387 | + | } | |||
| 374 | 388 | ||||
| 375 | return collectRoutesStatusAggregateSections(ctx, cfgPath, deps) | 389 | return collectRoutesStatusAggregateSections(ctx, cfgPath, deps) | ||
| 376 | } | 390 | } | ||
| @@ -585,6 +599,10 @@ func capitalizeKind(kind string) string { | |||||
| 585 | return strings.ToUpper(kind[:1]) + kind[1:] | 599 | return strings.ToUpper(kind[:1]) + kind[1:] | ||
| 586 | } | 600 | } | ||
| 587 | 601 | ||||
| 602 | + | func unresolvedRoutesTargetError(target string) string { | |||
| 603 | + | return fmt.Sprintf("could not resolve remote target %q", target) | |||
| 604 | + | } | |||
| 605 | + | ||||
| 588 | func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool) { | 606 | func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool) { | ||
| 589 | target, ok := resolveRoutesExplicitTarget() | 607 | target, ok := resolveRoutesExplicitTarget() | ||
| 590 | if !ok { | 608 | if !ok { | ||
M internal/adapters/in/cli/routes_test.go +88 -0
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "os" | |
| 7 | 7 | "path/filepath" | |
| 8 | 8 | "strings" | |
| 9 | + | "sync/atomic" | |
| 9 | 10 | "testing" | |
| 10 | 11 | ||
| 11 | 12 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | |
| @@ -340,6 +341,93 @@ func TestCollectRoutesListSections_ExplicitRemoteSkipsAggregate(t *testing.T) { | |||
| 340 | 341 | assert.Equal(t, "test.supri.xyz", sections[0].Routes[0].Domain) | |
| 341 | 342 | } | |
| 342 | 343 | ||
| 344 | + | func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *testing.T) { | |
| 345 | + | originalRemoteFlag := remoteFlag | |
| 346 | + | originalTokenFlag := tokenFlag | |
| 347 | + | originalInsecureTLSFlag := insecureTLSFlag | |
| 348 | + | t.Cleanup(func() { | |
| 349 | + | remoteFlag = originalRemoteFlag | |
| 350 | + | tokenFlag = originalTokenFlag | |
| 351 | + | insecureTLSFlag = originalInsecureTLSFlag | |
| 352 | + | }) | |
| 353 | + | ||
| 354 | + | remoteFlag = "missing-remote" | |
| 355 | + | tokenFlag = "" | |
| 356 | + | insecureTLSFlag = false | |
| 357 | + | configHome := t.TempDir() | |
| 358 | + | t.Setenv("XDG_CONFIG_HOME", configHome) | |
| 359 | + | t.Setenv("HOME", t.TempDir()) | |
| 360 | + | t.Setenv("GORDON_REMOTE", "") | |
| 361 | + | ||
| 362 | + | t.Run("list", func(t *testing.T) { | |
| 363 | + | var loadLocalCalls atomic.Int32 | |
| 364 | + | var listRemotesCalls atomic.Int32 | |
| 365 | + | var loadRemoteCalls atomic.Int32 | |
| 366 | + | ||
| 367 | + | deps := routesListDeps{ | |
| 368 | + | explicitRemote: resolveRoutesExplicitRemote, | |
| 369 | + | loadLocal: func(context.Context, string) (routeListSection, error) { | |
| 370 | + | loadLocalCalls.Add(1) | |
| 371 | + | return routeListSection{}, nil | |
| 372 | + | }, | |
| 373 | + | listRemotes: func() (map[string]remote.RemoteEntry, string, error) { | |
| 374 | + | listRemotesCalls.Add(1) | |
| 375 | + | return map[string]remote.RemoteEntry{}, "", nil | |
| 376 | + | }, | |
| 377 | + | loadRemote: func(context.Context, string, remote.RemoteEntry) (routeListSection, error) { | |
| 378 | + | loadRemoteCalls.Add(1) | |
| 379 | + | return routeListSection{}, nil | |
| 380 | + | }, | |
| 381 | + | } | |
| 382 | + | ||
| 383 | + | sections, err := collectRoutesListSections(context.Background(), "", deps) | |
| 384 | + | ||
| 385 | + | require.NoError(t, err) | |
| 386 | + | require.Len(t, sections, 1) | |
| 387 | + | assert.Equal(t, "remote", sections[0].Kind) | |
| 388 | + | assert.Equal(t, "missing-remote", sections[0].Name) | |
| 389 | + | require.NotEmpty(t, sections[0].Error) | |
| 390 | + | assert.Contains(t, sections[0].Error, "missing-remote") | |
| 391 | + | assert.Zero(t, loadLocalCalls.Load()) | |
| 392 | + | assert.Zero(t, listRemotesCalls.Load()) | |
| 393 | + | assert.Zero(t, loadRemoteCalls.Load()) | |
| 394 | + | }) | |
| 395 | + | ||
| 396 | + | t.Run("status", func(t *testing.T) { | |
| 397 | + | var loadLocalCalls atomic.Int32 | |
| 398 | + | var listRemotesCalls atomic.Int32 | |
| 399 | + | var loadRemoteCalls atomic.Int32 | |
| 400 | + | ||
| 401 | + | deps := routesStatusDeps{ | |
| 402 | + | explicitRemote: resolveRoutesExplicitRemote, | |
| 403 | + | loadLocal: func(context.Context, string) (routeStatusSection, error) { | |
| 404 | + | loadLocalCalls.Add(1) | |
| 405 | + | return routeStatusSection{}, nil | |
| 406 | + | }, | |
| 407 | + | listRemotes: func() (map[string]remote.RemoteEntry, string, error) { | |
| 408 | + | listRemotesCalls.Add(1) | |
| 409 | + | return map[string]remote.RemoteEntry{}, "", nil | |
| 410 | + | }, | |
| 411 | + | loadRemote: func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error) { | |
| 412 | + | loadRemoteCalls.Add(1) | |
| 413 | + | return routeStatusSection{}, nil | |
| 414 | + | }, | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | sections, err := collectRoutesStatusSections(context.Background(), "", deps) | |
| 418 | + | ||
| 419 | + | require.NoError(t, err) | |
| 420 | + | require.Len(t, sections, 1) | |
| 421 | + | assert.Equal(t, "remote", sections[0].Kind) | |
| 422 | + | assert.Equal(t, "missing-remote", sections[0].Name) | |
| 423 | + | require.NotEmpty(t, sections[0].Error) | |
| 424 | + | assert.Contains(t, sections[0].Error, "missing-remote") | |
| 425 | + | assert.Zero(t, loadLocalCalls.Load()) | |
| 426 | + | assert.Zero(t, listRemotesCalls.Load()) | |
| 427 | + | assert.Zero(t, loadRemoteCalls.Load()) | |
| 428 | + | }) | |
| 429 | + | } | |
| 430 | + | ||
| 343 | 431 | func TestRenderRoutesStatusSections_IncludesSectionHeadingsAndErrors(t *testing.T) { | |
| 344 | 432 | sections := []routeStatusSection{ | |
| 345 | 433 | { | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "os" | 6 | "os" | ||
| 7 | "path/filepath" | 7 | "path/filepath" | ||
| 8 | "strings" | 8 | "strings" | ||
| 9 | + | "sync/atomic" | |||
| 9 | "testing" | 10 | "testing" | ||
| 10 | 11 | ||||
| 11 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | 12 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | ||
| @@ -340,6 +341,93 @@ func TestCollectRoutesListSections_ExplicitRemoteSkipsAggregate(t *testing.T) { | |||||
| 340 | assert.Equal(t, "test.supri.xyz", sections[0].Routes[0].Domain) | 341 | assert.Equal(t, "test.supri.xyz", sections[0].Routes[0].Domain) | ||
| 341 | } | 342 | } | ||
| 342 | 343 | ||||
| 344 | + | func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *testing.T) { | |||
| 345 | + | originalRemoteFlag := remoteFlag | |||
| 346 | + | originalTokenFlag := tokenFlag | |||
| 347 | + | originalInsecureTLSFlag := insecureTLSFlag | |||
| 348 | + | t.Cleanup(func() { | |||
| 349 | + | remoteFlag = originalRemoteFlag | |||
| 350 | + | tokenFlag = originalTokenFlag | |||
| 351 | + | insecureTLSFlag = originalInsecureTLSFlag | |||
| 352 | + | }) | |||
| 353 | + | ||||
| 354 | + | remoteFlag = "missing-remote" | |||
| 355 | + | tokenFlag = "" | |||
| 356 | + | insecureTLSFlag = false | |||
| 357 | + | configHome := t.TempDir() | |||
| 358 | + | t.Setenv("XDG_CONFIG_HOME", configHome) | |||
| 359 | + | t.Setenv("HOME", t.TempDir()) | |||
| 360 | + | t.Setenv("GORDON_REMOTE", "") | |||
| 361 | + | ||||
| 362 | + | t.Run("list", func(t *testing.T) { | |||
| 363 | + | var loadLocalCalls atomic.Int32 | |||
| 364 | + | var listRemotesCalls atomic.Int32 | |||
| 365 | + | var loadRemoteCalls atomic.Int32 | |||
| 366 | + | ||||
| 367 | + | deps := routesListDeps{ | |||
| 368 | + | explicitRemote: resolveRoutesExplicitRemote, | |||
| 369 | + | loadLocal: func(context.Context, string) (routeListSection, error) { | |||
| 370 | + | loadLocalCalls.Add(1) | |||
| 371 | + | return routeListSection{}, nil | |||
| 372 | + | }, | |||
| 373 | + | listRemotes: func() (map[string]remote.RemoteEntry, string, error) { | |||
| 374 | + | listRemotesCalls.Add(1) | |||
| 375 | + | return map[string]remote.RemoteEntry{}, "", nil | |||
| 376 | + | }, | |||
| 377 | + | loadRemote: func(context.Context, string, remote.RemoteEntry) (routeListSection, error) { | |||
| 378 | + | loadRemoteCalls.Add(1) | |||
| 379 | + | return routeListSection{}, nil | |||
| 380 | + | }, | |||
| 381 | + | } | |||
| 382 | + | ||||
| 383 | + | sections, err := collectRoutesListSections(context.Background(), "", deps) | |||
| 384 | + | ||||
| 385 | + | require.NoError(t, err) | |||
| 386 | + | require.Len(t, sections, 1) | |||
| 387 | + | assert.Equal(t, "remote", sections[0].Kind) | |||
| 388 | + | assert.Equal(t, "missing-remote", sections[0].Name) | |||
| 389 | + | require.NotEmpty(t, sections[0].Error) | |||
| 390 | + | assert.Contains(t, sections[0].Error, "missing-remote") | |||
| 391 | + | assert.Zero(t, loadLocalCalls.Load()) | |||
| 392 | + | assert.Zero(t, listRemotesCalls.Load()) | |||
| 393 | + | assert.Zero(t, loadRemoteCalls.Load()) | |||
| 394 | + | }) | |||
| 395 | + | ||||
| 396 | + | t.Run("status", func(t *testing.T) { | |||
| 397 | + | var loadLocalCalls atomic.Int32 | |||
| 398 | + | var listRemotesCalls atomic.Int32 | |||
| 399 | + | var loadRemoteCalls atomic.Int32 | |||
| 400 | + | ||||
| 401 | + | deps := routesStatusDeps{ | |||
| 402 | + | explicitRemote: resolveRoutesExplicitRemote, | |||
| 403 | + | loadLocal: func(context.Context, string) (routeStatusSection, error) { | |||
| 404 | + | loadLocalCalls.Add(1) | |||
| 405 | + | return routeStatusSection{}, nil | |||
| 406 | + | }, | |||
| 407 | + | listRemotes: func() (map[string]remote.RemoteEntry, string, error) { | |||
| 408 | + | listRemotesCalls.Add(1) | |||
| 409 | + | return map[string]remote.RemoteEntry{}, "", nil | |||
| 410 | + | }, | |||
| 411 | + | loadRemote: func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error) { | |||
| 412 | + | loadRemoteCalls.Add(1) | |||
| 413 | + | return routeStatusSection{}, nil | |||
| 414 | + | }, | |||
| 415 | + | } | |||
| 416 | + | ||||
| 417 | + | sections, err := collectRoutesStatusSections(context.Background(), "", deps) | |||
| 418 | + | ||||
| 419 | + | require.NoError(t, err) | |||
| 420 | + | require.Len(t, sections, 1) | |||
| 421 | + | assert.Equal(t, "remote", sections[0].Kind) | |||
| 422 | + | assert.Equal(t, "missing-remote", sections[0].Name) | |||
| 423 | + | require.NotEmpty(t, sections[0].Error) | |||
| 424 | + | assert.Contains(t, sections[0].Error, "missing-remote") | |||
| 425 | + | assert.Zero(t, loadLocalCalls.Load()) | |||
| 426 | + | assert.Zero(t, listRemotesCalls.Load()) | |||
| 427 | + | assert.Zero(t, loadRemoteCalls.Load()) | |||
| 428 | + | }) | |||
| 429 | + | } | |||
| 430 | + | ||||
| 343 | func TestRenderRoutesStatusSections_IncludesSectionHeadingsAndErrors(t *testing.T) { | 431 | func TestRenderRoutesStatusSections_IncludesSectionHeadingsAndErrors(t *testing.T) { | ||
| 344 | sections := []routeStatusSection{ | 432 | sections := []routeStatusSection{ | ||
| 345 | { | 433 | { | ||