merge: resolve conflict with main in service_test.go
3 files · +188 -59
M internal/adapters/in/cli/bootstrap.go +74 -59
| @@ -1,7 +1,9 @@ | |||
| 1 | 1 | package cli | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "fmt" | |
| 6 | + | "io" | |
| 5 | 7 | "sort" | |
| 6 | 8 | "strings" | |
| 7 | 9 | ||
| @@ -21,66 +23,11 @@ func newBootstrapCmd() *cobra.Command { | |||
| 21 | 23 | Short: "Create a route, attachments, and secrets together", | |
| 22 | 24 | Args: cobra.ExactArgs(2), | |
| 23 | 25 | RunE: func(cmd *cobra.Command, args []string) error { | |
| 24 | - | ctx := cmd.Context() | |
| 25 | - | req := dto.BootstrapRequest{ | |
| 26 | - | Domain: args[0], | |
| 27 | - | Image: args[1], | |
| 28 | - | Attachments: append([]string(nil), attachments...), | |
| 29 | - | Env: map[string]string{}, | |
| 30 | - | AttachmentEnv: map[string]map[string]string{}, | |
| 31 | - | } | |
| 32 | - | ||
| 33 | - | for _, pair := range envPairs { | |
| 34 | - | parts := strings.SplitN(pair, "=", 2) | |
| 35 | - | if len(parts) != 2 { | |
| 36 | - | return fmt.Errorf("invalid --env value %q (expected KEY=VALUE)", pair) | |
| 37 | - | } | |
| 38 | - | req.Env[parts[0]] = parts[1] | |
| 39 | - | } | |
| 40 | - | ||
| 41 | - | for _, pair := range attachmentEnvPairs { | |
| 42 | - | serviceAndKV := strings.SplitN(pair, ":", 2) | |
| 43 | - | if len(serviceAndKV) != 2 { | |
| 44 | - | return fmt.Errorf("invalid --attachment-env value %q (expected service:KEY=VALUE)", pair) | |
| 45 | - | } | |
| 46 | - | keyValue := strings.SplitN(serviceAndKV[1], "=", 2) | |
| 47 | - | if len(keyValue) != 2 { | |
| 48 | - | return fmt.Errorf("invalid --attachment-env value %q (expected service:KEY=VALUE)", pair) | |
| 49 | - | } | |
| 50 | - | service := serviceAndKV[0] | |
| 51 | - | if req.AttachmentEnv[service] == nil { | |
| 52 | - | req.AttachmentEnv[service] = map[string]string{} | |
| 53 | - | } | |
| 54 | - | req.AttachmentEnv[service][keyValue[0]] = keyValue[1] | |
| 55 | - | } | |
| 56 | - | ||
| 57 | - | if len(req.Env) == 0 { | |
| 58 | - | req.Env = nil | |
| 59 | - | } | |
| 60 | - | if len(req.AttachmentEnv) == 0 { | |
| 61 | - | req.AttachmentEnv = nil | |
| 62 | - | } | |
| 63 | - | ||
| 64 | - | handle, err := resolveControlPlane(configPath) | |
| 26 | + | req, err := parseBootstrapRequest(args, attachments, envPairs, attachmentEnvPairs) | |
| 65 | 27 | if err != nil { | |
| 66 | 28 | return err | |
| 67 | 29 | } | |
| 68 | - | defer handle.close() | |
| 69 | - | ||
| 70 | - | resp, err := handle.plane.Bootstrap(ctx, req) | |
| 71 | - | if resp != nil { | |
| 72 | - | if writeErr := printBootstrapSummary(cmd, resp); writeErr != nil { | |
| 73 | - | return writeErr | |
| 74 | - | } | |
| 75 | - | } | |
| 76 | - | if err != nil { | |
| 77 | - | return fmt.Errorf("failed to bootstrap: %w", err) | |
| 78 | - | } | |
| 79 | - | out := cmd.OutOrStdout() | |
| 80 | - | if err := cliWriteLine(out, cliRenderSuccess("Bootstrap complete")); err != nil { | |
| 81 | - | return err | |
| 82 | - | } | |
| 83 | - | return cliWriteLine(out, fmt.Sprintf("Next: gordon push %s --build", req.Image)) | |
| 30 | + | return runBootstrap(cmd.Context(), req, configPath, cmd.OutOrStdout()) | |
| 84 | 31 | }, | |
| 85 | 32 | } | |
| 86 | 33 | ||
| @@ -92,8 +39,76 @@ func newBootstrapCmd() *cobra.Command { | |||
| 92 | 39 | return cmd | |
| 93 | 40 | } | |
| 94 | 41 | ||
| 95 | - | func printBootstrapSummary(cmd *cobra.Command, resp *dto.BootstrapResponse) error { | |
| 96 | - | out := cmd.OutOrStdout() | |
| 42 | + | func parseBootstrapRequest(args []string, attachments, envPairs, attachmentEnvPairs []string) (dto.BootstrapRequest, error) { | |
| 43 | + | req := dto.BootstrapRequest{ | |
| 44 | + | Domain: args[0], | |
| 45 | + | Image: args[1], | |
| 46 | + | Attachments: append([]string(nil), attachments...), | |
| 47 | + | Env: map[string]string{}, | |
| 48 | + | AttachmentEnv: map[string]map[string]string{}, | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | for _, pair := range envPairs { | |
| 52 | + | parts := strings.SplitN(pair, "=", 2) | |
| 53 | + | if len(parts) != 2 { | |
| 54 | + | return dto.BootstrapRequest{}, fmt.Errorf("invalid --env value %q (expected KEY=VALUE)", pair) | |
| 55 | + | } | |
| 56 | + | req.Env[parts[0]] = parts[1] | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | for _, pair := range attachmentEnvPairs { | |
| 60 | + | serviceAndKV := strings.SplitN(pair, ":", 2) | |
| 61 | + | if len(serviceAndKV) != 2 { | |
| 62 | + | return dto.BootstrapRequest{}, fmt.Errorf("invalid --attachment-env value %q (expected service:KEY=VALUE)", pair) | |
| 63 | + | } | |
| 64 | + | keyValue := strings.SplitN(serviceAndKV[1], "=", 2) | |
| 65 | + | if len(keyValue) != 2 { | |
| 66 | + | return dto.BootstrapRequest{}, fmt.Errorf("invalid --attachment-env value %q (expected service:KEY=VALUE)", pair) | |
| 67 | + | } | |
| 68 | + | service := serviceAndKV[0] | |
| 69 | + | if req.AttachmentEnv[service] == nil { | |
| 70 | + | req.AttachmentEnv[service] = map[string]string{} | |
| 71 | + | } | |
| 72 | + | req.AttachmentEnv[service][keyValue[0]] = keyValue[1] | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | if len(req.Env) == 0 { | |
| 76 | + | req.Env = nil | |
| 77 | + | } | |
| 78 | + | if len(req.AttachmentEnv) == 0 { | |
| 79 | + | req.AttachmentEnv = nil | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | return req, nil | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | func runBootstrap(ctx context.Context, req dto.BootstrapRequest, configPath string, out io.Writer) error { | |
| 86 | + | handle, err := resolveControlPlane(configPath) | |
| 87 | + | if err != nil { | |
| 88 | + | return err | |
| 89 | + | } | |
| 90 | + | defer handle.close() | |
| 91 | + | ||
| 92 | + | resp, err := handle.plane.Bootstrap(ctx, req) | |
| 93 | + | if resp != nil { | |
| 94 | + | if writeErr := printBootstrapSummary(out, resp); writeErr != nil { | |
| 95 | + | return writeErr | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | if err != nil { | |
| 99 | + | return fmt.Errorf("failed to bootstrap: %w", err) | |
| 100 | + | } | |
| 101 | + | if err := cliWriteLine(out, cliRenderSuccess("Bootstrap complete")); err != nil { | |
| 102 | + | return err | |
| 103 | + | } | |
| 104 | + | nextImage := req.Image | |
| 105 | + | if resp != nil && resp.Image != "" { | |
| 106 | + | nextImage = resp.Image | |
| 107 | + | } | |
| 108 | + | return cliWriteLine(out, fmt.Sprintf("Next: gordon push %s --build", nextImage)) | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func printBootstrapSummary(out io.Writer, resp *dto.BootstrapResponse) error { | |
| 97 | 112 | if err := cliWriteLine(out, cliRenderTitle("Bootstrap")); err != nil { | |
| 98 | 113 | return err | |
| 99 | 114 | } | |
| @@ -1,7 +1,9 @@ | |||||
| 1 | package cli | 1 | package cli | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "fmt" | 5 | "fmt" | ||
| 6 | + | "io" | |||
| 5 | "sort" | 7 | "sort" | ||
| 6 | "strings" | 8 | "strings" | ||
| 7 | 9 | ||||
| @@ -21,66 +23,11 @@ func newBootstrapCmd() *cobra.Command { | |||||
| 21 | Short: "Create a route, attachments, and secrets together", | 23 | Short: "Create a route, attachments, and secrets together", | ||
| 22 | Args: cobra.ExactArgs(2), | 24 | Args: cobra.ExactArgs(2), | ||
| 23 | RunE: func(cmd *cobra.Command, args []string) error { | 25 | RunE: func(cmd *cobra.Command, args []string) error { | ||
| 24 | - | ctx := cmd.Context() | 26 | + | req, err := parseBootstrapRequest(args, attachments, envPairs, attachmentEnvPairs) |
| 25 | - | req := dto.BootstrapRequest{ | |||
| 26 | - | Domain: args[0], | |||
| 27 | - | Image: args[1], | |||
| 28 | - | Attachments: append([]string(nil), attachments...), | |||
| 29 | - | Env: map[string]string{}, | |||
| 30 | - | AttachmentEnv: map[string]map[string]string{}, | |||
| 31 | - | } | |||
| 32 | - | ||||
| 33 | - | for _, pair := range envPairs { | |||
| 34 | - | parts := strings.SplitN(pair, "=", 2) | |||
| 35 | - | if len(parts) != 2 { | |||
| 36 | - | return fmt.Errorf("invalid --env value %q (expected KEY=VALUE)", pair) | |||
| 37 | - | } | |||
| 38 | - | req.Env[parts[0]] = parts[1] | |||
| 39 | - | } | |||
| 40 | - | ||||
| 41 | - | for _, pair := range attachmentEnvPairs { | |||
| 42 | - | serviceAndKV := strings.SplitN(pair, ":", 2) | |||
| 43 | - | if len(serviceAndKV) != 2 { | |||
| 44 | - | return fmt.Errorf("invalid --attachment-env value %q (expected service:KEY=VALUE)", pair) | |||
| 45 | - | } | |||
| 46 | - | keyValue := strings.SplitN(serviceAndKV[1], "=", 2) | |||
| 47 | - | if len(keyValue) != 2 { | |||
| 48 | - | return fmt.Errorf("invalid --attachment-env value %q (expected service:KEY=VALUE)", pair) | |||
| 49 | - | } | |||
| 50 | - | service := serviceAndKV[0] | |||
| 51 | - | if req.AttachmentEnv[service] == nil { | |||
| 52 | - | req.AttachmentEnv[service] = map[string]string{} | |||
| 53 | - | } | |||
| 54 | - | req.AttachmentEnv[service][keyValue[0]] = keyValue[1] | |||
| 55 | - | } | |||
| 56 | - | ||||
| 57 | - | if len(req.Env) == 0 { | |||
| 58 | - | req.Env = nil | |||
| 59 | - | } | |||
| 60 | - | if len(req.AttachmentEnv) == 0 { | |||
| 61 | - | req.AttachmentEnv = nil | |||
| 62 | - | } | |||
| 63 | - | ||||
| 64 | - | handle, err := resolveControlPlane(configPath) | |||
| 65 | if err != nil { | 27 | if err != nil { | ||
| 66 | return err | 28 | return err | ||
| 67 | } | 29 | } | ||
| 68 | - | defer handle.close() | 30 | + | return runBootstrap(cmd.Context(), req, configPath, cmd.OutOrStdout()) |
| 69 | - | ||||
| 70 | - | resp, err := handle.plane.Bootstrap(ctx, req) | |||
| 71 | - | if resp != nil { | |||
| 72 | - | if writeErr := printBootstrapSummary(cmd, resp); writeErr != nil { | |||
| 73 | - | return writeErr | |||
| 74 | - | } | |||
| 75 | - | } | |||
| 76 | - | if err != nil { | |||
| 77 | - | return fmt.Errorf("failed to bootstrap: %w", err) | |||
| 78 | - | } | |||
| 79 | - | out := cmd.OutOrStdout() | |||
| 80 | - | if err := cliWriteLine(out, cliRenderSuccess("Bootstrap complete")); err != nil { | |||
| 81 | - | return err | |||
| 82 | - | } | |||
| 83 | - | return cliWriteLine(out, fmt.Sprintf("Next: gordon push %s --build", req.Image)) | |||
| 84 | }, | 31 | }, | ||
| 85 | } | 32 | } | ||
| 86 | 33 | ||||
| @@ -92,8 +39,76 @@ func newBootstrapCmd() *cobra.Command { | |||||
| 92 | return cmd | 39 | return cmd | ||
| 93 | } | 40 | } | ||
| 94 | 41 | ||||
| 95 | - | func printBootstrapSummary(cmd *cobra.Command, resp *dto.BootstrapResponse) error { | 42 | + | func parseBootstrapRequest(args []string, attachments, envPairs, attachmentEnvPairs []string) (dto.BootstrapRequest, error) { |
| 96 | - | out := cmd.OutOrStdout() | 43 | + | req := dto.BootstrapRequest{ |
| 44 | + | Domain: args[0], | |||
| 45 | + | Image: args[1], | |||
| 46 | + | Attachments: append([]string(nil), attachments...), | |||
| 47 | + | Env: map[string]string{}, | |||
| 48 | + | AttachmentEnv: map[string]map[string]string{}, | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | for _, pair := range envPairs { | |||
| 52 | + | parts := strings.SplitN(pair, "=", 2) | |||
| 53 | + | if len(parts) != 2 { | |||
| 54 | + | return dto.BootstrapRequest{}, fmt.Errorf("invalid --env value %q (expected KEY=VALUE)", pair) | |||
| 55 | + | } | |||
| 56 | + | req.Env[parts[0]] = parts[1] | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | for _, pair := range attachmentEnvPairs { | |||
| 60 | + | serviceAndKV := strings.SplitN(pair, ":", 2) | |||
| 61 | + | if len(serviceAndKV) != 2 { | |||
| 62 | + | return dto.BootstrapRequest{}, fmt.Errorf("invalid --attachment-env value %q (expected service:KEY=VALUE)", pair) | |||
| 63 | + | } | |||
| 64 | + | keyValue := strings.SplitN(serviceAndKV[1], "=", 2) | |||
| 65 | + | if len(keyValue) != 2 { | |||
| 66 | + | return dto.BootstrapRequest{}, fmt.Errorf("invalid --attachment-env value %q (expected service:KEY=VALUE)", pair) | |||
| 67 | + | } | |||
| 68 | + | service := serviceAndKV[0] | |||
| 69 | + | if req.AttachmentEnv[service] == nil { | |||
| 70 | + | req.AttachmentEnv[service] = map[string]string{} | |||
| 71 | + | } | |||
| 72 | + | req.AttachmentEnv[service][keyValue[0]] = keyValue[1] | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | if len(req.Env) == 0 { | |||
| 76 | + | req.Env = nil | |||
| 77 | + | } | |||
| 78 | + | if len(req.AttachmentEnv) == 0 { | |||
| 79 | + | req.AttachmentEnv = nil | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | return req, nil | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | func runBootstrap(ctx context.Context, req dto.BootstrapRequest, configPath string, out io.Writer) error { | |||
| 86 | + | handle, err := resolveControlPlane(configPath) | |||
| 87 | + | if err != nil { | |||
| 88 | + | return err | |||
| 89 | + | } | |||
| 90 | + | defer handle.close() | |||
| 91 | + | ||||
| 92 | + | resp, err := handle.plane.Bootstrap(ctx, req) | |||
| 93 | + | if resp != nil { | |||
| 94 | + | if writeErr := printBootstrapSummary(out, resp); writeErr != nil { | |||
| 95 | + | return writeErr | |||
| 96 | + | } | |||
| 97 | + | } | |||
| 98 | + | if err != nil { | |||
| 99 | + | return fmt.Errorf("failed to bootstrap: %w", err) | |||
| 100 | + | } | |||
| 101 | + | if err := cliWriteLine(out, cliRenderSuccess("Bootstrap complete")); err != nil { | |||
| 102 | + | return err | |||
| 103 | + | } | |||
| 104 | + | nextImage := req.Image | |||
| 105 | + | if resp != nil && resp.Image != "" { | |||
| 106 | + | nextImage = resp.Image | |||
| 107 | + | } | |||
| 108 | + | return cliWriteLine(out, fmt.Sprintf("Next: gordon push %s --build", nextImage)) | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func printBootstrapSummary(out io.Writer, resp *dto.BootstrapResponse) error { | |||
| 97 | if err := cliWriteLine(out, cliRenderTitle("Bootstrap")); err != nil { | 112 | if err := cliWriteLine(out, cliRenderTitle("Bootstrap")); err != nil { | ||
| 98 | return err | 113 | return err | ||
| 99 | } | 114 | } | ||
M internal/usecase/config/service.go +21 -0
| @@ -515,9 +515,30 @@ type configSnapshot struct { | |||
| 515 | 515 | values map[string]any | |
| 516 | 516 | } | |
| 517 | 517 | ||
| 518 | + | // mutableSections lists config sections that are intentionally modified by service operations. | |
| 519 | + | var mutableSections = []string{"routes.", "external_routes.", "attachments.", "network_groups."} | |
| 520 | + | ||
| 521 | + | func isMutableKey(key string) bool { | |
| 522 | + | for _, prefix := range mutableSections { | |
| 523 | + | if strings.HasPrefix(key, prefix) { | |
| 524 | + | return true | |
| 525 | + | } | |
| 526 | + | } | |
| 527 | + | ||
| 528 | + | switch key { | |
| 529 | + | case "routes", "external_routes", "attachments", "network_groups": | |
| 530 | + | return true | |
| 531 | + | } | |
| 532 | + | ||
| 533 | + | return false | |
| 534 | + | } | |
| 535 | + | ||
| 518 | 536 | func (s *Service) snapshotCriticalFields() configSnapshot { | |
| 519 | 537 | snap := configSnapshot{values: make(map[string]any)} | |
| 520 | 538 | for _, key := range s.viper.AllKeys() { | |
| 539 | + | if isMutableKey(key) { | |
| 540 | + | continue | |
| 541 | + | } | |
| 521 | 542 | snap.values[key] = s.viper.Get(key) | |
| 522 | 543 | } | |
| 523 | 544 | return snap | |
| @@ -515,9 +515,30 @@ type configSnapshot struct { | |||||
| 515 | values map[string]any | 515 | values map[string]any | ||
| 516 | } | 516 | } | ||
| 517 | 517 | ||||
| 518 | + | // mutableSections lists config sections that are intentionally modified by service operations. | |||
| 519 | + | var mutableSections = []string{"routes.", "external_routes.", "attachments.", "network_groups."} | |||
| 520 | + | ||||
| 521 | + | func isMutableKey(key string) bool { | |||
| 522 | + | for _, prefix := range mutableSections { | |||
| 523 | + | if strings.HasPrefix(key, prefix) { | |||
| 524 | + | return true | |||
| 525 | + | } | |||
| 526 | + | } | |||
| 527 | + | ||||
| 528 | + | switch key { | |||
| 529 | + | case "routes", "external_routes", "attachments", "network_groups": | |||
| 530 | + | return true | |||
| 531 | + | } | |||
| 532 | + | ||||
| 533 | + | return false | |||
| 534 | + | } | |||
| 535 | + | ||||
| 518 | func (s *Service) snapshotCriticalFields() configSnapshot { | 536 | func (s *Service) snapshotCriticalFields() configSnapshot { | ||
| 519 | snap := configSnapshot{values: make(map[string]any)} | 537 | snap := configSnapshot{values: make(map[string]any)} | ||
| 520 | for _, key := range s.viper.AllKeys() { | 538 | for _, key := range s.viper.AllKeys() { | ||
| 539 | + | if isMutableKey(key) { | |||
| 540 | + | continue | |||
| 541 | + | } | |||
| 521 | snap.values[key] = s.viper.Get(key) | 542 | snap.values[key] = s.viper.Get(key) | ||
| 522 | } | 543 | } | ||
| 523 | return snap | 544 | return snap | ||
M internal/usecase/config/service_test.go +93 -0
| @@ -1409,6 +1409,99 @@ port = 9999 | |||
| 1409 | 1409 | assert.Equal(t, 5, len(matches)) | |
| 1410 | 1410 | }) | |
| 1411 | 1411 | ||
| 1412 | + | t.Run("AddRoute updates existing dotted domain route", func(t *testing.T) { | |
| 1413 | + | tmpDir := t.TempDir() | |
| 1414 | + | configFile := filepath.Join(tmpDir, "gordon.toml") | |
| 1415 | + | initialConfig := ` | |
| 1416 | + | [server] | |
| 1417 | + | port = 8088 | |
| 1418 | + | ||
| 1419 | + | [auth] | |
| 1420 | + | username = "testadmin" | |
| 1421 | + | token_secret = "testsecret123" | |
| 1422 | + | ||
| 1423 | + | [routes] | |
| 1424 | + | "pitlane.example.com" = "pitlane:latest" | |
| 1425 | + | ` | |
| 1426 | + | err := os.WriteFile(configFile, []byte(initialConfig), 0600) | |
| 1427 | + | require.NoError(t, err) | |
| 1428 | + | ||
| 1429 | + | v := viper.New() | |
| 1430 | + | v.SetConfigFile(configFile) | |
| 1431 | + | err = v.ReadInConfig() | |
| 1432 | + | require.NoError(t, err) | |
| 1433 | + | ||
| 1434 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 1435 | + | svc := NewService(v, eventBus) | |
| 1436 | + | ctx := testContext() | |
| 1437 | + | err = svc.Load(ctx) | |
| 1438 | + | require.NoError(t, err) | |
| 1439 | + | ||
| 1440 | + | err = svc.AddRoute(ctx, domain.Route{ | |
| 1441 | + | Domain: "pitlane.example.com", | |
| 1442 | + | Image: "reg.example.com/pitlane:latest", | |
| 1443 | + | }) | |
| 1444 | + | require.NoError(t, err, "AddRoute should succeed when updating an existing dotted-domain route") | |
| 1445 | + | ||
| 1446 | + | routes := svc.GetRoutes(ctx) | |
| 1447 | + | require.Len(t, routes, 1) | |
| 1448 | + | assert.Equal(t, "pitlane.example.com", routes[0].Domain) | |
| 1449 | + | assert.Equal(t, "reg.example.com/pitlane:latest", routes[0].Image) | |
| 1450 | + | ||
| 1451 | + | v2 := viper.New() | |
| 1452 | + | v2.SetConfigFile(configFile) | |
| 1453 | + | err = v2.ReadInConfig() | |
| 1454 | + | require.NoError(t, err) | |
| 1455 | + | assert.Equal(t, 8088, v2.GetInt("server.port")) | |
| 1456 | + | assert.Equal(t, "testadmin", v2.GetString("auth.username")) | |
| 1457 | + | assert.Equal(t, "testsecret123", v2.GetString("auth.token_secret")) | |
| 1458 | + | }) | |
| 1459 | + | ||
| 1460 | + | t.Run("RemoveRoute removes dotted domain route", func(t *testing.T) { | |
| 1461 | + | tmpDir := t.TempDir() | |
| 1462 | + | configFile := filepath.Join(tmpDir, "gordon.toml") | |
| 1463 | + | initialConfig := ` | |
| 1464 | + | [server] | |
| 1465 | + | port = 8088 | |
| 1466 | + | ||
| 1467 | + | [auth] | |
| 1468 | + | username = "testadmin" | |
| 1469 | + | token_secret = "testsecret123" | |
| 1470 | + | ||
| 1471 | + | [routes] | |
| 1472 | + | "pitlane.example.com" = "reg.example.com/pitlane:latest" | |
| 1473 | + | "other.example.com" = "reg.example.com/other:latest" | |
| 1474 | + | ` | |
| 1475 | + | err := os.WriteFile(configFile, []byte(initialConfig), 0600) | |
| 1476 | + | require.NoError(t, err) | |
| 1477 | + | ||
| 1478 | + | v := viper.New() | |
| 1479 | + | v.SetConfigFile(configFile) | |
| 1480 | + | err = v.ReadInConfig() | |
| 1481 | + | require.NoError(t, err) | |
| 1482 | + | ||
| 1483 | + | eventBus := mocks.NewMockEventPublisher(t) | |
| 1484 | + | svc := NewService(v, eventBus) | |
| 1485 | + | ctx := testContext() | |
| 1486 | + | err = svc.Load(ctx) | |
| 1487 | + | require.NoError(t, err) | |
| 1488 | + | ||
| 1489 | + | err = svc.RemoveRoute(ctx, "pitlane.example.com") | |
| 1490 | + | require.NoError(t, err, "RemoveRoute should succeed for a dotted-domain route") | |
| 1491 | + | ||
| 1492 | + | routes := svc.GetRoutes(ctx) | |
| 1493 | + | require.Len(t, routes, 1) | |
| 1494 | + | assert.Equal(t, "other.example.com", routes[0].Domain, "other routes should be preserved") | |
| 1495 | + | assert.Equal(t, "reg.example.com/other:latest", routes[0].Image, "other routes should be preserved") | |
| 1496 | + | ||
| 1497 | + | v2 := viper.New() | |
| 1498 | + | v2.SetConfigFile(configFile) | |
| 1499 | + | err = v2.ReadInConfig() | |
| 1500 | + | require.NoError(t, err) | |
| 1501 | + | assert.Equal(t, 8088, v2.GetInt("server.port")) | |
| 1502 | + | assert.Equal(t, "testadmin", v2.GetString("auth.username")) | |
| 1503 | + | assert.Equal(t, "testsecret123", v2.GetString("auth.token_secret")) | |
| 1504 | + | }) | |
| 1412 | 1505 | t.Run("network_groups are persisted to disk", func(t *testing.T) { | |
| 1413 | 1506 | // Setup: config file with auth, server, and network_groups | |
| 1414 | 1507 | tmpDir := t.TempDir() | |
| @@ -1409,6 +1409,99 @@ port = 9999 | |||||
| 1409 | assert.Equal(t, 5, len(matches)) | 1409 | assert.Equal(t, 5, len(matches)) | ||
| 1410 | }) | 1410 | }) | ||
| 1411 | 1411 | ||||
| 1412 | + | t.Run("AddRoute updates existing dotted domain route", func(t *testing.T) { | |||
| 1413 | + | tmpDir := t.TempDir() | |||
| 1414 | + | configFile := filepath.Join(tmpDir, "gordon.toml") | |||
| 1415 | + | initialConfig := ` | |||
| 1416 | + | [server] | |||
| 1417 | + | port = 8088 | |||
| 1418 | + | ||||
| 1419 | + | [auth] | |||
| 1420 | + | username = "testadmin" | |||
| 1421 | + | token_secret = "testsecret123" | |||
| 1422 | + | ||||
| 1423 | + | [routes] | |||
| 1424 | + | "pitlane.example.com" = "pitlane:latest" | |||
| 1425 | + | ` | |||
| 1426 | + | err := os.WriteFile(configFile, []byte(initialConfig), 0600) | |||
| 1427 | + | require.NoError(t, err) | |||
| 1428 | + | ||||
| 1429 | + | v := viper.New() | |||
| 1430 | + | v.SetConfigFile(configFile) | |||
| 1431 | + | err = v.ReadInConfig() | |||
| 1432 | + | require.NoError(t, err) | |||
| 1433 | + | ||||
| 1434 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 1435 | + | svc := NewService(v, eventBus) | |||
| 1436 | + | ctx := testContext() | |||
| 1437 | + | err = svc.Load(ctx) | |||
| 1438 | + | require.NoError(t, err) | |||
| 1439 | + | ||||
| 1440 | + | err = svc.AddRoute(ctx, domain.Route{ | |||
| 1441 | + | Domain: "pitlane.example.com", | |||
| 1442 | + | Image: "reg.example.com/pitlane:latest", | |||
| 1443 | + | }) | |||
| 1444 | + | require.NoError(t, err, "AddRoute should succeed when updating an existing dotted-domain route") | |||
| 1445 | + | ||||
| 1446 | + | routes := svc.GetRoutes(ctx) | |||
| 1447 | + | require.Len(t, routes, 1) | |||
| 1448 | + | assert.Equal(t, "pitlane.example.com", routes[0].Domain) | |||
| 1449 | + | assert.Equal(t, "reg.example.com/pitlane:latest", routes[0].Image) | |||
| 1450 | + | ||||
| 1451 | + | v2 := viper.New() | |||
| 1452 | + | v2.SetConfigFile(configFile) | |||
| 1453 | + | err = v2.ReadInConfig() | |||
| 1454 | + | require.NoError(t, err) | |||
| 1455 | + | assert.Equal(t, 8088, v2.GetInt("server.port")) | |||
| 1456 | + | assert.Equal(t, "testadmin", v2.GetString("auth.username")) | |||
| 1457 | + | assert.Equal(t, "testsecret123", v2.GetString("auth.token_secret")) | |||
| 1458 | + | }) | |||
| 1459 | + | ||||
| 1460 | + | t.Run("RemoveRoute removes dotted domain route", func(t *testing.T) { | |||
| 1461 | + | tmpDir := t.TempDir() | |||
| 1462 | + | configFile := filepath.Join(tmpDir, "gordon.toml") | |||
| 1463 | + | initialConfig := ` | |||
| 1464 | + | [server] | |||
| 1465 | + | port = 8088 | |||
| 1466 | + | ||||
| 1467 | + | [auth] | |||
| 1468 | + | username = "testadmin" | |||
| 1469 | + | token_secret = "testsecret123" | |||
| 1470 | + | ||||
| 1471 | + | [routes] | |||
| 1472 | + | "pitlane.example.com" = "reg.example.com/pitlane:latest" | |||
| 1473 | + | "other.example.com" = "reg.example.com/other:latest" | |||
| 1474 | + | ` | |||
| 1475 | + | err := os.WriteFile(configFile, []byte(initialConfig), 0600) | |||
| 1476 | + | require.NoError(t, err) | |||
| 1477 | + | ||||
| 1478 | + | v := viper.New() | |||
| 1479 | + | v.SetConfigFile(configFile) | |||
| 1480 | + | err = v.ReadInConfig() | |||
| 1481 | + | require.NoError(t, err) | |||
| 1482 | + | ||||
| 1483 | + | eventBus := mocks.NewMockEventPublisher(t) | |||
| 1484 | + | svc := NewService(v, eventBus) | |||
| 1485 | + | ctx := testContext() | |||
| 1486 | + | err = svc.Load(ctx) | |||
| 1487 | + | require.NoError(t, err) | |||
| 1488 | + | ||||
| 1489 | + | err = svc.RemoveRoute(ctx, "pitlane.example.com") | |||
| 1490 | + | require.NoError(t, err, "RemoveRoute should succeed for a dotted-domain route") | |||
| 1491 | + | ||||
| 1492 | + | routes := svc.GetRoutes(ctx) | |||
| 1493 | + | require.Len(t, routes, 1) | |||
| 1494 | + | assert.Equal(t, "other.example.com", routes[0].Domain, "other routes should be preserved") | |||
| 1495 | + | assert.Equal(t, "reg.example.com/other:latest", routes[0].Image, "other routes should be preserved") | |||
| 1496 | + | ||||
| 1497 | + | v2 := viper.New() | |||
| 1498 | + | v2.SetConfigFile(configFile) | |||
| 1499 | + | err = v2.ReadInConfig() | |||
| 1500 | + | require.NoError(t, err) | |||
| 1501 | + | assert.Equal(t, 8088, v2.GetInt("server.port")) | |||
| 1502 | + | assert.Equal(t, "testadmin", v2.GetString("auth.username")) | |||
| 1503 | + | assert.Equal(t, "testsecret123", v2.GetString("auth.token_secret")) | |||
| 1504 | + | }) | |||
| 1412 | t.Run("network_groups are persisted to disk", func(t *testing.T) { | 1505 | t.Run("network_groups are persisted to disk", func(t *testing.T) { | ||
| 1413 | // Setup: config file with auth, server, and network_groups | 1506 | // Setup: config file with auth, server, and network_groups | ||
| 1414 | tmpDir := t.TempDir() | 1507 | tmpDir := t.TempDir() | ||