Skip to content

Commit e381e9e

Browse files
committed
fix: expand global proxy without losing projects
- recreate proxy with the union of listener ports and networks - restore complete Caddy apps after listener expansion - clear global network across recursive directory contexts
1 parent e59bc40 commit e381e9e

4 files changed

Lines changed: 107 additions & 1 deletion

File tree

commands/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ func clearDirectoryEnvironment(directory string) func() {
234234
"KOOL_WORKSPACE_SOURCE", "KOOL_WORKSPACE_SOURCE_PROJECT", "KOOL_WORKSPACE_NAME",
235235
"KOOL_WORKSPACE_PATH", "KOOL_WORKSPACE_PROJECT", "KOOL_WORKSPACE_SERVICES",
236236
"KOOL_PROXY_DOMAIN", "KOOL_PROXY_HOST", "COMPOSE_PROJECT_NAME", "COMPOSE_FILE",
237+
"KOOL_GLOBAL_NETWORK",
237238
}
238239
keys = append(keys, environment.LoadedEnvKeys(directory)...)
239240
for _, key := range keys {

commands/root_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,30 @@ func TestRecursiveWorkingDirectoryReplacesParentEnvironment(t *testing.T) {
275275
}
276276
}
277277

278+
func TestRecursiveWorkingDirectoryReplacesGlobalNetwork(t *testing.T) {
279+
originalDirectory, err := os.Getwd()
280+
if err != nil {
281+
t.Fatal(err)
282+
}
283+
parent := t.TempDir()
284+
target := t.TempDir()
285+
if err = os.WriteFile(filepath.Join(target, ".env"), []byte("KOOL_GLOBAL_NETWORK=target_network\n"), 0644); err != nil {
286+
t.Fatal(err)
287+
}
288+
t.Setenv("KOOL_GLOBAL_NETWORK", "kool_global")
289+
restore := clearDirectoryEnvironment(parent)
290+
defer restore()
291+
if err := os.Chdir(target); err != nil {
292+
t.Fatal(err)
293+
}
294+
t.Cleanup(func() { _ = os.Chdir(originalDirectory) })
295+
env := environment.NewEnvStorage()
296+
environment.InitEnvironmentVariables(env)
297+
if got := env.Get("KOOL_GLOBAL_NETWORK"); got != "target_network" {
298+
t.Fatalf("expected target global network, got %q", got)
299+
}
300+
}
301+
278302
func TestRecursiveCall(t *testing.T) {
279303
recursive := &cobra.Command{
280304
Use: "recursive",

services/proxy/manager.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,10 @@ func (m *DefaultManager) createAliasOverride(network string, routes []route) (st
431431
func (m *DefaultManager) ensureCaddy(network string, routes []route) error {
432432
inspect := builder.NewCommand("docker", "inspect", "--format", "{{.State.Running}}", caddyContainer)
433433
running, err := m.shell.Exec(inspect)
434+
ports := make(map[int]bool)
435+
var preservedApps []byte
436+
var preservedAppsExist bool
437+
var preservedNetworks []string
434438
if err == nil {
435439
compatibility, inspectErr := m.shell.Exec(builder.NewCommand("docker", "inspect", "--format", "{{json .NetworkSettings.Networks}}|{{json .Config.Entrypoint}}|{{json .Config.Cmd}}", caddyContainer))
436440
if inspectErr != nil {
@@ -442,6 +446,33 @@ func (m *DefaultManager) ensureCaddy(network string, routes []route) error {
442446
return err
443447
}
444448
err = errors.New("legacy proxy container removed")
449+
} else {
450+
for _, route := range routes {
451+
if _, portErr := m.shell.Exec(builder.NewCommand("docker", "port", caddyContainer), fmt.Sprintf("%d/tcp", route.Listen)); portErr == nil {
452+
ports[route.Listen] = true
453+
continue
454+
}
455+
if preservedApps, preservedAppsExist, err = m.snapshotApps(); err != nil {
456+
return err
457+
}
458+
bindings, inspectErr := m.shell.Exec(builder.NewCommand("docker", "inspect", "--format", "{{json .HostConfig.PortBindings}}", caddyContainer))
459+
if inspectErr != nil {
460+
return inspectErr
461+
}
462+
for port := range parseContainerPorts(bindings) {
463+
ports[port] = true
464+
}
465+
networks, inspectErr := m.shell.Exec(builder.NewCommand("docker", "inspect", "--format", "{{json .NetworkSettings.Networks}}", caddyContainer))
466+
if inspectErr != nil {
467+
return inspectErr
468+
}
469+
preservedNetworks = parseDockerObjectKeys(networks)
470+
if err = m.shell.Interactive(builder.NewCommand("docker", "rm", "--force"), caddyContainer); err != nil {
471+
return err
472+
}
473+
err = errors.New("proxy listener expansion required")
474+
break
475+
}
445476
}
446477
}
447478
if err != nil {
@@ -455,7 +486,6 @@ func (m *DefaultManager) ensureCaddy(network string, routes []route) error {
455486
}
456487
}
457488
args := []string{"run", "-d", "--name", caddyContainer, "--restart", "unless-stopped", "--network", caddyAdminNet, "--network-alias", caddyAdminHost, "-p", "127.0.0.1:2019:2019"}
458-
ports := make(map[int]bool)
459489
for _, route := range routes {
460490
ports[route.Listen] = true
461491
}
@@ -494,6 +524,14 @@ func (m *DefaultManager) ensureCaddy(network string, routes []route) error {
494524
return err
495525
}
496526
}
527+
for _, previousNetwork := range preservedNetworks {
528+
if previousNetwork == caddyAdminNet || previousNetwork == network {
529+
continue
530+
}
531+
if err = m.shell.Interactive(builder.NewCommand("docker", "network", "connect", previousNetwork), caddyContainer); err != nil {
532+
return err
533+
}
534+
}
497535

498536
for _, route := range routes {
499537
if _, err = m.shell.Exec(builder.NewCommand("docker", "port", caddyContainer), fmt.Sprintf("%d/tcp", route.Listen)); err != nil {
@@ -505,6 +543,9 @@ func (m *DefaultManager) ensureCaddy(network string, routes []route) error {
505543
if requestErr == nil {
506544
_ = response.Body.Close()
507545
if response.StatusCode < 500 {
546+
if preservedAppsExist {
547+
return m.restoreApps(preservedApps, true)
548+
}
508549
return nil
509550
}
510551
}
@@ -513,6 +554,34 @@ func (m *DefaultManager) ensureCaddy(network string, routes []route) error {
513554
return errors.New("kool proxy did not become ready")
514555
}
515556

557+
func parseContainerPorts(raw string) map[int]bool {
558+
ports := make(map[int]bool)
559+
var bindings map[string]interface{}
560+
if json.Unmarshal([]byte(raw), &bindings) != nil {
561+
return ports
562+
}
563+
for key := range bindings {
564+
port, err := strconv.Atoi(strings.TrimSuffix(key, "/tcp"))
565+
if err == nil {
566+
ports[port] = true
567+
}
568+
}
569+
return ports
570+
}
571+
572+
func parseDockerObjectKeys(raw string) []string {
573+
var object map[string]interface{}
574+
if json.Unmarshal([]byte(raw), &object) != nil {
575+
return nil
576+
}
577+
keys := make([]string, 0, len(object))
578+
for key := range object {
579+
keys = append(keys, key)
580+
}
581+
sort.Strings(keys)
582+
return keys
583+
}
584+
516585
func (m *DefaultManager) ensureBaseConfig() (string, error) {
517586
home, err := os.UserHomeDir()
518587
if err != nil {

services/proxy/manager_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,18 @@ func TestProxyCompatibilityRequiresSecureStoredCommand(t *testing.T) {
758758
}
759759
}
760760

761+
func TestParseExistingProxyPortsAndNetworks(t *testing.T) {
762+
ports := parseContainerPorts(`{"80/tcp":[{}],"3001/tcp":[{}]}`)
763+
if !ports[80] || !ports[3001] || len(ports) != 2 {
764+
t.Fatalf("expected existing proxy ports to be preserved, got %v", ports)
765+
}
766+
networks := parseDockerObjectKeys(`{"project_b":{},"kool_proxy_admin":{},"project_a":{}}`)
767+
expected := []string{"kool_proxy_admin", "project_a", "project_b"}
768+
if strings.Join(networks, ",") != strings.Join(expected, ",") {
769+
t.Fatalf("expected sorted existing networks %v, got %v", expected, networks)
770+
}
771+
}
772+
761773
func TestRestoreAppsIfUnchangedPreservesConcurrentUpdate(t *testing.T) {
762774
current := []byte(`{"http":{"servers":{"current":{}}}}`)
763775
restored := false

0 commit comments

Comments
 (0)