From 8143879116245a34266bc84c0ab1bf128a6188db Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Mon, 19 Oct 2026 20:46:47 -0300 Subject: [PATCH 1/3] fix(proxy): generate DNS-safe upstream aliases --- services/proxy/manager.go | 38 +++++++++++++- services/proxy/manager_test.go | 93 ++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/services/proxy/manager.go b/services/proxy/manager.go index 033a4191..9b45f364 100644 --- a/services/proxy/manager.go +++ b/services/proxy/manager.go @@ -1648,7 +1648,43 @@ func (m *DefaultManager) request(method, url string, body []byte) (*http.Respons } func (m *DefaultManager) alias(service string) string { - return m.project() + "-" + service + project := m.project() + name := project + "-" + service + const maxLabelLength = 63 + valid := len(name) <= maxLabelLength && name[0] != '-' && name[len(name)-1] != '-' + for _, character := range name { + if !dnsLabelCharacter(character) { + valid = false + break + } + } + if valid { + return name + } + + // Hash the complete project/service pair, not the truncated or normalized + // label. Reserve space for the hash after including the service suffix. + digest := sha256.Sum256([]byte(project + "\x00" + service)) + suffix := fmt.Sprintf("-%x", digest[:8]) + prefix := strings.Map(func(character rune) rune { + if dnsLabelCharacter(character) { + return character + } + return '-' + }, name) + prefix = strings.Trim(prefix, "-") + if len(prefix) > maxLabelLength-len(suffix) { + prefix = strings.TrimRight(prefix[:maxLabelLength-len(suffix)], "-") + } + if prefix == "" { + prefix = "kool" + } + return prefix + suffix +} + +func dnsLabelCharacter(character rune) bool { + return character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' || + character >= '0' && character <= '9' || character == '-' } func (m *DefaultManager) routeID(route route) string { diff --git a/services/proxy/manager_test.go b/services/proxy/manager_test.go index 889111ef..5c5eee8c 100644 --- a/services/proxy/manager_test.go +++ b/services/proxy/manager_test.go @@ -13,6 +13,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "strings" "testing" ) @@ -128,6 +129,98 @@ func TestLoadConfigRejectsReservedAdminPort(t *testing.T) { } } +func TestAlias(t *testing.T) { + longPrefix := strings.Repeat("a", 80) + tests := []struct { + name, project, service string + unchanged bool + }{ + {"ordinary", "example", "app", true}, + {"63 characters", strings.Repeat("p", 59), "app", true}, + {"64 characters", strings.Repeat("p", 60), "app", false}, + {"reported workspace", "exlink-workspace-lattice-special-education-category-e6bb1c8c", "app", false}, + {"long project a", longPrefix + "a", "app", false}, + {"long project b", longPrefix + "b", "app", false}, + {"long service a", "example", longPrefix + "a", false}, + {"long service b", "example", longPrefix + "b", false}, + {"both long", longPrefix, longPrefix, false}, + {"underscore", "example_project", "web_app", false}, + {"normalization collision", "example-project", "web_app", false}, + {"dot", "example.project", "app", false}, + {"leading hyphen", "-example", "app", false}, + {"trailing hyphen", "example", "app-", false}, + {"unicode", "例子", "应用", false}, + {"empty", "", "", false}, + {"uppercase", "Example", "App", true}, + {"numeric", "123", "456", true}, + {"truncated pair a", longPrefix + "-web", "app", false}, + {"truncated pair b", longPrefix, "web-app", false}, + } + label := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`) + seen := make(map[string]string) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + manager := testRouteManager("", test.project, "app.localhost") + alias := manager.alias(test.service) + if len(alias) > 63 || !label.MatchString(alias) { + t.Fatalf("invalid DNS label %q (%d bytes)", alias, len(alias)) + } + original := test.project + "-" + test.service + if (alias == original) != test.unchanged { + t.Errorf("alias %q, original %q, want unchanged=%v", alias, original, test.unchanged) + } + if again := manager.alias(test.service); again != alias { + t.Errorf("alias is not deterministic: %q != %q", again, alias) + } + if previous, exists := seen[alias]; exists { + t.Errorf("alias collision with %s: %q", previous, alias) + } + seen[alias] = test.name + }) + } +} + +func TestLongAliasMatchesDockerAndCaddy(t *testing.T) { + for _, test := range []struct { + name, project, service string + }{ + {"workspace", "exlink-workspace-lattice-special-education-category-e6bb1c8c", "app"}, + {"long service", "example", strings.Repeat("service", 20)}, + } { + t.Run(test.name, func(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + state, server := newCaddyRouteState(t) + manager := testRouteManager(server.URL, "source", "workspace.app.localhost") + manager.env.Set("KOOL_WORKSPACE", "true") + manager.env.Set("KOOL_WORKSPACE_PROJECT", test.project) + workDir := t.TempDir() + manager.env.Set("PWD", workDir) + if err := os.WriteFile(filepath.Join(workDir, "compose.yml"), []byte("services: {}\n"), 0644); err != nil { + t.Fatal(err) + } + proxyRoute := route{Service: test.service, Listen: 80, Target: 8080, Hosts: []string{"@"}} + file, err := manager.createAliasOverride(defaultNetwork, []route{proxyRoute}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Remove(file) }) + content, err := os.ReadFile(file) + if err != nil { + t.Fatal(err) + } + alias := manager.alias(test.service) + if len(alias) > 63 || !strings.Contains(string(content), fmt.Sprintf(" - %q\n", alias)) { + t.Fatalf("expected DNS-safe alias %q in Compose override:\n%s", alias, content) + } + mustRegister(t, manager, proxyRoute) + state.requireUpstream(t, "kool-80", manager.routeID(proxyRoute), alias+":8080") + if routes := state.routes["kool-80"]; len(routes) != 1 || !strings.Contains(string(routes[0]), `"workspace.app.localhost"`) { + t.Fatalf("browser hostname changed: %s", routes) + } + }) + } +} + func TestCreateAliasOverride(t *testing.T) { workDir := t.TempDir() if err := os.WriteFile(filepath.Join(workDir, "compose.yml"), []byte("services: {}\n"), 0644); err != nil { From 5cad5d7436b6e205c87a34843d5caa477cc82395 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Mon, 19 Oct 2026 20:58:32 -0300 Subject: [PATCH 2/3] fix(ci): build Docker image with patched Go 1.26.9 --- Dockerfile | 2 +- docs/01-Getting-Started/5-CI-Integration.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 84354eae..291c4074 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25 AS build +FROM golang:1.26.9 AS build ARG BUILD_VERSION=0.0.0-auto diff --git a/docs/01-Getting-Started/5-CI-Integration.md b/docs/01-Getting-Started/5-CI-Integration.md index 55f1ece9..7e479102 100644 --- a/docs/01-Getting-Started/5-CI-Integration.md +++ b/docs/01-Getting-Started/5-CI-Integration.md @@ -12,7 +12,7 @@ If your CI runner already has Docker available on the host (e.g. GitHub Actions' ## What's in the image -- `kool` CLI — built with Go 1.25, statically linked +- `kool` CLI — built with Go 1.26.9, statically linked - `docker` CLI - `docker compose` plugin (Compose V2) - `git` and `bash` From fef5cb701c1c3f509f21782634dbed9ab5b8f806 Mon Sep 17 00:00:00 2001 From: Daniel Polito Date: Tue, 20 Oct 2026 11:30:25 -0300 Subject: [PATCH 3/3] feat(proxy): use compact hashed aliases and expose debug mappings --- commands/proxy.go | 28 ++++++++ commands/proxy_test.go | 116 +++++++++++++++++++++++++++++++++ docs/15-Snippets/Workspaces.md | 15 +++++ services/proxy/manager.go | 69 +++++++++----------- services/proxy/manager_test.go | 92 +++++++++++++++++--------- 5 files changed, 251 insertions(+), 69 deletions(-) create mode 100644 commands/proxy_test.go diff --git a/commands/proxy.go b/commands/proxy.go index 90ce6807..467871dc 100644 --- a/commands/proxy.go +++ b/commands/proxy.go @@ -26,5 +26,33 @@ func AddKoolProxy(root *cobra.Command) { return proxy.NewManager(sh, environment.NewEnvStorage()).Trust() }, }) + proxyCommand.AddCommand(newProxyAliasesCommand(environment.NewEnvStorage())) root.AddCommand(proxyCommand) } + +func newProxyAliasesCommand(env environment.EnvStorage) *cobra.Command { + return &cobra.Command{ + Use: "aliases", + Short: "List configured internal proxy aliases for the current project", + Long: "List configured internal Docker DNS aliases for the current project. This does not inspect running containers or contact Caddy.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + aliases, err := proxy.NewManager(shell.NewShell(), env).Aliases() + if err != nil { + return err + } + if len(aliases) == 0 { + cmd.Println("No proxy aliases configured.") + return nil + } + table := shell.NewTableWriter() + table.SetWriter(cmd.OutOrStdout()) + table.AppendHeader("Project", "Service", "Network", "Alias") + for _, alias := range aliases { + table.AppendRow(alias.Project, alias.Service, alias.Network, alias.Name) + } + table.Render() + return nil + }, + } +} diff --git a/commands/proxy_test.go b/commands/proxy_test.go new file mode 100644 index 00000000..eb10928f --- /dev/null +++ b/commands/proxy_test.go @@ -0,0 +1,116 @@ +package commands + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestProxyAliasesCommand(t *testing.T) { + config := `proxy: + domain: app.localhost + network: shared + routes: + node: + ports: ["3001:3001"] + app: + ports: ["80:8080", "8081:8081"] +` + for _, workspace := range []bool{false, true} { + name := "source" + if workspace { + name = "workspace" + } + t.Run(name, func(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "kool.yml"), []byte(config), 0644); err != nil { + t.Fatal(err) + } + t.Setenv("PWD", dir) + t.Setenv("KOOL_NAME", "fallback") + t.Setenv("COMPOSE_PROJECT_NAME", "compose-fallback") + t.Setenv("KOOL_WORKSPACE_SOURCE_PROJECT", "example") + t.Setenv("KOOL_WORKSPACE_PROJECT", "example-workspace-task-a") + t.Setenv("KOOL_PROXY_HOST", "app.localhost") + t.Setenv("KOOL_WORKSPACE", "false") + project, alias := "example", "kool-2c16ea1198025827" + if workspace { + t.Setenv("KOOL_WORKSPACE", "true") + project, alias = "example-workspace-task-a", "kool-1c11a4b25a7f4f2e" + } + // Listing configured names must not require Docker or Caddy. + t.Setenv("PATH", t.TempDir()) + root := &cobra.Command{Use: "kool"} + AddKoolProxy(root) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"proxy", "aliases"}) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + output := out.String() + for _, expected := range []string{"PROJECT", "SERVICE", "NETWORK", "ALIAS", project, "shared", "app", "node", alias} { + if !strings.Contains(output, expected) { + t.Errorf("expected %q in output:\n%s", expected, output) + } + } + if strings.Count(output, alias) != 1 { + t.Fatalf("multiple ports must not duplicate a service alias:\n%s", output) + } + if strings.Index(output, "app") > strings.Index(output, "node") { + t.Fatalf("expected services sorted by name:\n%s", output) + } + }) + } +} + +func TestProxyAliasesCommandEmptyAndInvalidConfig(t *testing.T) { + for _, test := range []struct { + name, config, want string + wantError bool + }{ + {"missing config", "", "No proxy aliases configured.", false}, + {"no proxy", "scripts: {}\n", "No proxy aliases configured.", false}, + {"empty routes", "proxy:\n domain: app.localhost\n", "No proxy aliases configured.", false}, + {"invalid config", "proxy:\n domain: app.localhost\n routes:\n app:\n ports: [invalid]\n", "listen:target", true}, + } { + t.Run(test.name, func(t *testing.T) { + dir := t.TempDir() + if test.config != "" { + if err := os.WriteFile(filepath.Join(dir, "kool.yml"), []byte(test.config), 0644); err != nil { + t.Fatal(err) + } + } + t.Setenv("PWD", dir) + t.Setenv("PATH", t.TempDir()) + root := &cobra.Command{Use: "kool"} + AddKoolProxy(root) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"proxy", "aliases"}) + err := root.Execute() + if test.wantError { + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("expected %q error, got %v", test.want, err) + } + } else if err != nil || !strings.Contains(out.String(), test.want) { + t.Fatalf("expected %q, got %q, error: %v", test.want, out.String(), err) + } + }) + } +} + +func TestProxyAliasesCommandRejectsArguments(t *testing.T) { + root := &cobra.Command{Use: "kool"} + AddKoolProxy(root) + root.SetArgs([]string{"proxy", "aliases", "app"}) + if err := root.Execute(); err == nil { + t.Fatal("expected aliases to reject positional arguments") + } +} diff --git a/docs/15-Snippets/Workspaces.md b/docs/15-Snippets/Workspaces.md index af913516..df0c798a 100644 --- a/docs/15-Snippets/Workspaces.md +++ b/docs/15-Snippets/Workspaces.md @@ -108,6 +108,21 @@ environment: No Kool or proxy labels are required in `docker-compose.yml`. +### Debugging internal proxy aliases + +Kool gives every proxied service a stable, 21-character Docker network alias: +`kool-` followed by a hash of the full Compose project and service names. This +keeps upstream DNS names short even for long workspace or service names. Browser +hostnames and Compose service names are unchanged. + +Run `kool proxy aliases` in the source project or workspace to see its configured +project, service, network, and alias mappings. The command does not require Docker +or Caddy and shows intended configuration, not live container state. + +When upgrading from project/service-based aliases, recreate all proxied services +with `kool start` in each project/workspace so Docker and Caddy use the new names +together. + ### Vite assets and HMR Configure Vite to use the public origin passed by Compose: diff --git a/services/proxy/manager.go b/services/proxy/manager.go index 9b45f364..8ea468d9 100644 --- a/services/proxy/manager.go +++ b/services/proxy/manager.go @@ -43,6 +43,15 @@ type Manager interface { Remove([]string) error RemoveProject(string, []string) error Trust() error + Aliases() ([]Alias, error) +} + +// Alias describes a configured proxy service's internal Docker DNS name. +type Alias struct { + Project string + Service string + Network string + Name string } type route struct { @@ -300,6 +309,23 @@ func (m *DefaultManager) Trust() error { } } +// Aliases lists the current project's configured aliases without contacting Docker or Caddy. +func (m *DefaultManager) Aliases() ([]Alias, error) { + cfg, err := m.loadConfig() + if err != nil || cfg == nil { + return nil, err + } + var aliases []Alias + seen := make(map[string]bool) + for _, route := range cfg.Routes { + if !seen[route.Service] { + aliases = append(aliases, Alias{Project: m.project(), Service: route.Service, Network: cfg.Network, Name: m.alias(route.Service)}) + seen[route.Service] = true + } + } + return aliases, nil +} + func (m *DefaultManager) loadConfig() (*config, error) { parsed, err := parser.LoadKoolYaml(m.env.Get("PWD")) if errors.Is(err, parser.ErrKoolYmlNotFound) { @@ -1020,6 +1046,8 @@ func (m *DefaultManager) removeProjectRoutesUnlocked(services []string) error { aliases := make(map[string]bool, len(services)) for _, service := range services { aliases[m.alias(service)] = true + // Also allow stopping routes created before aliases were hashed. + aliases[m.project()+"-"+service] = true } return m.filterProjectRoutesUnlocked(func(route caddyRouteMetadata) bool { if len(aliases) == 0 { @@ -1648,43 +1676,10 @@ func (m *DefaultManager) request(method, url string, body []byte) (*http.Respons } func (m *DefaultManager) alias(service string) string { - project := m.project() - name := project + "-" + service - const maxLabelLength = 63 - valid := len(name) <= maxLabelLength && name[0] != '-' && name[len(name)-1] != '-' - for _, character := range name { - if !dnsLabelCharacter(character) { - valid = false - break - } - } - if valid { - return name - } - - // Hash the complete project/service pair, not the truncated or normalized - // label. Reserve space for the hash after including the service suffix. - digest := sha256.Sum256([]byte(project + "\x00" + service)) - suffix := fmt.Sprintf("-%x", digest[:8]) - prefix := strings.Map(func(character rune) rune { - if dnsLabelCharacter(character) { - return character - } - return '-' - }, name) - prefix = strings.Trim(prefix, "-") - if len(prefix) > maxLabelLength-len(suffix) { - prefix = strings.TrimRight(prefix[:maxLabelLength-len(suffix)], "-") - } - if prefix == "" { - prefix = "kool" - } - return prefix + suffix -} - -func dnsLabelCharacter(character rune) bool { - return character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' || - character >= '0' && character <= '9' || character == '-' + // Separate the complete project/service pair so ambiguous concatenations + // cannot share an alias. A 64-bit digest keeps every label short and DNS-safe. + digest := sha256.Sum256([]byte(m.project() + "\x00" + service)) + return fmt.Sprintf("kool-%x", digest[:8]) } func (m *DefaultManager) routeID(route route) string { diff --git a/services/proxy/manager_test.go b/services/proxy/manager_test.go index 5c5eee8c..28ab7268 100644 --- a/services/proxy/manager_test.go +++ b/services/proxy/manager_test.go @@ -133,43 +133,45 @@ func TestAlias(t *testing.T) { longPrefix := strings.Repeat("a", 80) tests := []struct { name, project, service string - unchanged bool }{ - {"ordinary", "example", "app", true}, - {"63 characters", strings.Repeat("p", 59), "app", true}, - {"64 characters", strings.Repeat("p", 60), "app", false}, - {"reported workspace", "exlink-workspace-lattice-special-education-category-e6bb1c8c", "app", false}, - {"long project a", longPrefix + "a", "app", false}, - {"long project b", longPrefix + "b", "app", false}, - {"long service a", "example", longPrefix + "a", false}, - {"long service b", "example", longPrefix + "b", false}, - {"both long", longPrefix, longPrefix, false}, - {"underscore", "example_project", "web_app", false}, - {"normalization collision", "example-project", "web_app", false}, - {"dot", "example.project", "app", false}, - {"leading hyphen", "-example", "app", false}, - {"trailing hyphen", "example", "app-", false}, - {"unicode", "例子", "应用", false}, - {"empty", "", "", false}, - {"uppercase", "Example", "App", true}, - {"numeric", "123", "456", true}, - {"truncated pair a", longPrefix + "-web", "app", false}, - {"truncated pair b", longPrefix, "web-app", false}, - } - label := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`) + {"ordinary", "example", "app"}, + {"different service", "example", "node"}, + {"different project", "other", "app"}, + {"63 characters", strings.Repeat("p", 59), "app"}, + {"64 characters", strings.Repeat("p", 60), "app"}, + {"reported workspace", "exlink-workspace-lattice-special-education-category-e6bb1c8c", "app"}, + {"long project a", longPrefix + "a", "app"}, + {"long project b", longPrefix + "b", "app"}, + {"long service a", "example", longPrefix + "a"}, + {"long service b", "example", longPrefix + "b"}, + {"both long", longPrefix, longPrefix}, + {"underscore", "example_project", "web_app"}, + {"normalization collision", "example-project", "web_app"}, + {"dot", "example.project", "app"}, + {"leading hyphen", "-example", "app"}, + {"trailing hyphen", "example", "app-"}, + {"unicode", "例子", "应用"}, + {"empty", "", ""}, + {"uppercase", "Example", "App"}, + {"numeric", "123", "456"}, + {"ambiguous pair a", "example-web", "app"}, + {"ambiguous pair b", "example", "web-app"}, + {"truncated pair a", longPrefix + "-web", "app"}, + {"truncated pair b", longPrefix, "web-app"}, + } + label := regexp.MustCompile(`^kool-[0-9a-f]{16}$`) seen := make(map[string]string) for _, test := range tests { t.Run(test.name, func(t *testing.T) { manager := testRouteManager("", test.project, "app.localhost") alias := manager.alias(test.service) - if len(alias) > 63 || !label.MatchString(alias) { + if len(alias) != 21 || !label.MatchString(alias) { t.Fatalf("invalid DNS label %q (%d bytes)", alias, len(alias)) } - original := test.project + "-" + test.service - if (alias == original) != test.unchanged { - t.Errorf("alias %q, original %q, want unchanged=%v", alias, original, test.unchanged) + if test.name == "ordinary" && alias != "kool-2c16ea1198025827" { + t.Errorf("alias algorithm changed: %q", alias) } - if again := manager.alias(test.service); again != alias { + if again := testRouteManager("", test.project, "other.localhost").alias(test.service); again != alias { t.Errorf("alias is not deterministic: %q != %q", again, alias) } if previous, exists := seen[alias]; exists { @@ -180,10 +182,11 @@ func TestAlias(t *testing.T) { } } -func TestLongAliasMatchesDockerAndCaddy(t *testing.T) { +func TestAliasMatchesDockerAndCaddy(t *testing.T) { for _, test := range []struct { name, project, service string }{ + {"ordinary", "example", "app"}, {"workspace", "exlink-workspace-lattice-special-education-category-e6bb1c8c", "app"}, {"long service", "example", strings.Repeat("service", 20)}, } { @@ -240,7 +243,7 @@ func TestCreateAliasOverride(t *testing.T) { if err != nil { t.Fatal(err) } - if !strings.Contains(string(content), "example-app") || !strings.Contains(string(content), "kool_global") { + if !strings.Contains(string(content), "kool-2c16ea1198025827") || !strings.Contains(string(content), "kool_global") { t.Errorf("unexpected alias override:\n%s", content) } if strings.Count(string(content), `"app":`) != 1 { @@ -394,7 +397,7 @@ func TestRegisterWorkspaceRoute(t *testing.T) { } encoded, _ := json.Marshal(registered) result := string(encoded) - for _, expected := range []string{"task-a.workspace.app.localhost", "*.task-a.workspace.app.localhost", "example-workspace-task-a-app:8080"} { + for _, expected := range []string{"task-a.workspace.app.localhost", "*.task-a.workspace.app.localhost", "kool-1c11a4b25a7f4f2e:8080"} { if !strings.Contains(result, expected) { t.Errorf("expected registered route to contain %q, got %s", expected, result) } @@ -1175,6 +1178,31 @@ func TestRestoreAppsIfUnchangedPreservesConcurrentUpdate(t *testing.T) { } } +func TestRemoveServiceRoutesSupportsLegacyAliases(t *testing.T) { + for _, legacy := range []bool{false, true} { + t.Run(fmt.Sprintf("legacy=%v", legacy), func(t *testing.T) { + state, server := newCaddyRouteState(t) + manager := testRouteManager(server.URL, "example", "app.localhost") + app := route{Service: "app", Listen: 80, Target: 8080, Hosts: []string{"@"}} + node := route{Service: "node", Listen: 3001, Target: 3001, Hosts: []string{"@"}} + mustRegister(t, manager, app) + mustRegister(t, manager, node) + other := testRouteManager(server.URL, "other", "other.localhost") + mustRegister(t, other, app) + if legacy { + for index, raw := range state.routes["kool-80"] { + state.routes["kool-80"][index] = json.RawMessage(strings.ReplaceAll(string(raw), manager.alias("app")+":8080", "example-app:8080")) + } + } + if err := manager.removeProjectRoutes([]string{"app"}); err != nil { + t.Fatal(err) + } + state.requireRoutes(t, "kool-80", []string{other.routeID(app)}) + state.requireRoutes(t, "kool-3001", []string{manager.routeID(node)}) + }) + } +} + func TestWorkspaceRoutePrecedence(t *testing.T) { proxyRoute := route{Service: "app", Listen: 80, Target: 80, Hosts: []string{"@", "*"}} viteRoute := route{Service: "node", Listen: 3001, Target: 3001, Hosts: []string{"@", "*"}} @@ -1191,7 +1219,7 @@ func TestWorkspaceRoutePrecedence(t *testing.T) { "kool-exlink-workspace-task-a-app-80-80", "kool-exlink-app-80-80", }) - state.requireUpstream(t, "kool-80", "kool-exlink-workspace-task-a-app-80-80", "exlink-workspace-task-a-app:80") + state.requireUpstream(t, "kool-80", "kool-exlink-workspace-task-a-app-80-80", "kool-dc6777eafc0a9c53:80") }) t.Run("workspace started before and routes re-registered", func(t *testing.T) {